在運維與開發中,常需「查看日誌尾部」或「分頁倒序讀取」。將整個檔案載入記憶體不現實,需使用 RandomAccessFile 從檔案末尾向前讀取。
end:讀取終點位移(byte),null 代表從檔案最末尾開始。num:欲讀取的行數。file:目標檔案物件。charset:字元編碼,預設 UTF-8。// 讀取最後 10 行
Map result = FileUtil.tail(null, 10, file, null);
List<Object[]> lines = (List) result.get(FileUtil.ARR);
// 接著向上再讀 3 行(第 11~13 行)
FileUtil.tail((Long) result.get(FileUtil.POINT), 3, file, null);
FileUtil.tail()public class FileUtil {
private static final long STEP = 5000; // 每次向前讀取的位元組塊大小
public static final String ARR = "arr"; // 回傳行資料
public static final String POINT = "point"; // 下一次讀取的起始位移
public static Map<String, Object> tail(Long end, long num, File file, String charset) throws Exception {
if (num <= 0 || (end != null && end < 0)) throw new IllegalArgumentException();
Map<String, Object> map = new HashMap<>();
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "r");
long fileEnd = (end == null ? raf.length() : end);
long readPos = fileEnd > STEP ? fileEnd - STEP : 0;
raf.seek(readPos);
LinkedList<Object[]> queue = new LinkedList<>(); // 固定大小隊列,存放 [位移, 行內容]
String line;
int skippedFirst = 0;
while ((line = raf.readLine()) != null) {
// 若非從 0 開始讀,第一行通常不完整,跳過
if (skippedFirst++ == 0 && readPos != 0) continue;
long pointer = raf.getFilePointer();
if (pointer >= fileEnd && end != null) break; // 超過指定終點
Object[] entry = new Object[2];
entry[0] = pointer;
entry[1] = new String(line.getBytes("ISO-8859-1"), charset == null ? "UTF-8" : charset);
if (queue.size() == num) queue.poll(); // 維持固定大小
queue.offer(entry);
}
// 讀不夠 num 行且還有資料,遞迴向前繼續讀
if (queue.size() < num && readPos > 0) {
long need = num - queue.size();
Object[] first = queue.peek();
if (first == null) throw new RuntimeException("STEP 太小,無法定位完整行");
Map<String, Object> more = tail((Long) first[0], need, file, charset);
map.put(POINT, more.get(POINT));
map.put(ARR, ArrayUtils.addAll((Object[]) more.get(ARR), queue.toArray()));
} else if (!queue.isEmpty()) {
map.put(POINT, queue.peek()[0]);
map.put(ARR, queue.toArray());
}
} finally {
if (raf != null) try { raf.close(); } catch (IOException ignored) {}
}
return map;
}
}
STEP 位元組,用 LinkedList 維持最新 num 行。readLine() 讀到的是片段,直接丟棄。readLine() 回傳 ISO-8859-1 位元組陣列,再用目標編碼重組字串。end 遞迴呼叫。finally 區塊確保 RandomAccessFile 關閉。