如何重置輸入流並再次讀取文件
1. 概述
Java 使用輸入流作為資料輸入/輸出 (I/O) 操作的抽象層。我們可以將串流與各種資料來源一起使用,例如檔案、記憶體或網路。顧名思義,「流」的資料流是單向的,就像管道一樣。這是使用流最有效的方式。然而,有時我們需要返回到流中的其他資料。
在本教程中,我們將學習如何在資料流中標記一個位置,以便稍後返回該位置。
2. InputStream類
抽象類別InputStream提供了一個基本 API。我們有三個函數來管理流的行為:
-
mark()– 標記流中的位置。它接受預讀限制,該限制指示我們可以安全讀取並傳回的後續位元組數。 -
reset()– 使返回到標記位置之後的下一個位置。 -
markSupported()– 如果特定流支援標記和重置操作,則傳回true
從InputStream類別中,我們可以取得這些函數的預設實作。讓我們來看一下:
-
markSupported()– 回傳false -
reset()– 拋出IOException異常 -
mark()– 無效
任何繼承自InputStream類別都會以相同的方式運行,除非它實作了自己的這些函數版本。
3. 重置ByteArrayInputStream
我們使用ByteArrayInputStream類別從記憶體中讀取資料。它支援重置,因此其markSupported()方法傳回 true。接下來,讓我們來看一個簡單的例子來了解mark()和reset()方法是如何協同工作的:
@Test
void givenByteArrayInputStream_whenMarkAndReset_thenReadMarkedPosition() {
final int EXPECTED_NUMBER = 3;
byte[] buffer = { 1, 2, 3, 4, 5 };
ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
int number = bis.read(); //get 1
number = bis.read(); //get 2
bis.mark(0); //irrelevant for ByteArrayInputStream
number = bis.read(); //get 3
number = bis.read(); //get 4
bis.reset();
number = bis.read(); //should get 3
assertEquals(EXPECTED_NUMBER, number);
}
由於我們在第二次讀取後呼叫了mark()函數,所以我們安排了一次重置操作,重置位置剛好是下一個位置,也就是第三個位置。然後,我們實際上是在執行了兩次額外的讀取操作之後才執行的。
值得注意的是,對於ByteArrayInputStream來說,預讀限制毫無意義。這是因為一旦流創建完成, 所有資料都可用。我們透過將零傳遞給[mark()](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/io/ByteArrayInputStream.html#mark(int)) .
4. BufferedInputStream類
為了在從檔案讀取資料時跳到上一層目錄,我們可以使用BufferedInputStream類別。我們可以將此類視為FileInputStream物件的容器:
@Test
void givenBufferedInputStream_whenMarkAndReset_thenReadMarkedPosition() throws IOException {
final int readLimit = 500;
final char EXPECTED_CHAR = 'w';
FileInputStream fis = new FileInputStream(fileName);
//content:
//All work and no play makes Jack a dull boy
BufferedInputStream bis = new BufferedInputStream(fis);
bis.read(); // A
bis.read(); // l
bis.read(); // l
bis.read(); // space
bis.mark(readLimit); // at w
bis.read();
bis.read();
bis.reset();
char test = (char) bis.read();
assertEquals(EXPECTED_CHAR, test);
}
值得注意的是,我們將FileInputStream實例傳遞給了BufferedInputStream建構子。此外,這種流實作要求向mark()傳遞一個有意義的預讀限制。
5. 標記位置無效
讀取過多位元組會導致標記位置無效,進而導致reset()函數失效:
@Test
void givenBufferedInputStream_whenMarkIsInvalid_thenIOException() throws IOException {
final int bufferSize = 2;
final int readLimit = 1;
assertThrows(IOException.class, () -> {
FileInputStream fis = new FileInputStream(fileName);
// constructor accepting buffer size
BufferedInputStream bis = new BufferedInputStream(fis, bufferSize);
bis.read();
bis.mark(readLimit);
bis.read();
bis.read();
bis.read(); // this read exceeds both read limit and buffer size
bis.reset(); // mark position is invalid
});
}
在這個例子中,我們使用了BufferedInputStream建構函數,它接受內部緩衝區的初始大小。當預讀限制和緩衝區大小都超過限制時,呼叫reset()會拋出IOException異常。
6. RandomAccessFile替代方案
利用RandomAccessFile類,我們可以隨機地以讀取或寫入模式存取檔案。因此,我們可以自由地選擇對文件進行操作的位置。這顯然不同於流的概念,流假定的是單向處理。
讓我們用RandomAccessFile函數來模擬流的mark() / reset()對的工作:
@Test
void givenRandomAccessFile_whenSeek_thenMoveToIndicatedPosition() throws IOException {
final char EXPECTED_CHAR = 'w';
RandomAccessFile raf = new RandomAccessFile(fileName, "r"); //open file in read mode
//content:
//All work and no play makes Jack a dull boy
raf.read(); // A
raf.read(); // l
raf.read(); // l
raf.read(); // space
long filePointer = raf.getFilePointer(); //at w
raf.read();
raf.read();
raf.read();
raf.read();
raf.seek(filePointer);
int test = raf.read();
assertEquals(test, EXPECTED_CHAR);
}
我們結合使用了getFilePointer()函數和seek()函數。 getFilePointer getFilePointer()傳回檔案中的目前偏移量,而seek()將偏移量設定為提供的值。
7. 結論
本文介紹了讀取檔案時如何進行回退操作。首先,我們重點介紹了InputStream類別及其子類別。我們檢查了特定流是否支援回退操作。然後,我們示範了回退操作的工作原理。最後,作為流方法的替代方案,我們研究了RandomAccessFile類,它支援對文件進行隨機存取。
和往常一樣,範例程式碼可在 GitHub 上找到。