不幸的是,事实并非如此。
这段代码有两个主要问题。首先我会解决简单的问题,但最重要的是第二点。
编码问题
String line = randomAccessFile.readLine();
这一行将字节隐式转换为字符,这通常是个坏主意,因为字节不是字符,从一个字节转换为另一个字节需要字符集编码。
这种方法(来自 RAF 的readLine())是一个奇怪的案例 - 可能是因为 RandomAccessFile 是非常古老的 API。使用此方法将应用一些 bizarro ISO-8859-1 esque 字符集编码:它通过将每个字节作为一个完整的字符来将字节转换为字符,假设字节表示列出的 unicode 字符,这实际上不是一个理智的编码,只是一个懒惰的程序员。
您的结果是:除非您可以保证此日志文件始终只包含 ASCII 字符,否则此代码已损坏,readLine 根本无法使用。相反,您将不得不做更多的工作:读取字节直到换行,然后将收集到的字节转换为带有new String(byteArray, StandardCharsets.UTF_8) 的字符串,或者使用ByteBuffer 并应用类似的策略。但请继续阅读,因为解决第二个问题会自动解决这个问题。
缓冲
现代计算机系统倾向于喜欢“打包”。您不能真正对单个字节进行操作。以 SSD 为例(尽管这也适用于旋转盘片):实际的 SSD 硬件无法读取单个字节。它只能读取整个块的数据。
因此,当您明确向操作系统请求单个字节时,最终会引发一系列事件,导致 SSD 读取整个块,然后将整个块传递给操作系统,然后操作系统将忽略除你想要的一个字节,然后返回那个。
如果您的代码随后请求下一个字节,我们将再次执行该例程。
因此,如果您从具有 1024 字节块的 SSD 连续读取 1024 字节,则通过调用 read() 1024 次会导致 SSD 执行 1024 次读取,而调用 read(byteArr) 一次,会传递一个 1024-字节数组,使 SSD 执行单次读取。
是的,这意味着字节数组解决方案实际上快了 1000 倍。
这同样适用于网络。一千次发送 1 个字节通常比发送一次 1000 个字节慢近 1000 倍; TCP/IP 数据包可以携带大约 1800 字节的数据,因此发送少于该数据的数据几乎不会为您带来任何好处。
RAF 的readLine() 的工作方式类似于第一个(坏)场景:它一次读取一个字节,直到遇到换行符。因此,要读取 100 个字符的字符串,比只知道需要读取 100 个字符并一口气读取它们要慢 100 倍。
解决方案
您可能想完全放弃 RandomAccessFile,它是相当旧的 API。
缓冲的一个主要问题是,除非您事先知道要读取多少字节,否则缓冲要困难得多。在这里,您不知道:您想继续阅读,直到遇到换行符为止,但是您不知道要多久才能到达那里。此外,缓冲 API 往往只返回方便的内容,因此可能读取的字节数比我们要求的要少(但它总是至少读取 1,除非我们到达文件末尾)。因此,我们需要编写代码来重复读取整个块的数据,分析块中是否有换行符,如果不存在,请继续阅读。
此外,打开渠道等费用昂贵。所以,如果你想挖掘所有的日志行,编写每次都打开一个新通道的代码是次优的。
这个怎么样,使用来自java.nio.file的较新的文件API:
public class LogLineReader implements AutoCloseable {
private final byte[] buffer = new byte[1024];
private final ByteBuffer bb = wrap(buffer);
private final SeekableByteChannel channel;
private final Charset charset = StandardCharsets.UTF_8;
public LogLineReader(Path p) {
channel = Files.newByteChannel(p, StandardOpenOption.READ);
channel.position(111L); // you seek to pos 111 in your code...
}
@Override public void close() throws IOException {
channel.close();
}
// This code buffers: First, our internal buffer is scanned
// for a new line. If there is no full line in the buffer,
// we read bytes from the file and check again until we find one.
public String readLine() {
int len = 0;
if (!channel.isOpen()) return null;
int scanStart = 0;
while (true) {
// Scan through the bytes we have buffered for a newline.
for (int i = scanStart; i < buffer.position(); i++) {
if (buffer[i] == '\n') {
// Found it. Take all bytes up to the new line, turn into
// a string.
String res = new String(buffer, 0, i, charset);
// Copy all bytes from _after_ the newline to the front.
System.arraycopy(buffer, i + 1, buffer, 0, buffer.position() - i - 1);
// Adjust the position (which represents how many bytes are buffered).
buffer.position(buffer.position() - i - 1);
return res;
}
}
scanStart = buffer.position();
// If we get here, the buffer is empty or contains no newline.
if (scanStart == buffer.limit()) {
throw new IOException("Log line too long");
}
int read = channel.read(buffer); // let's fetch more bytes!
if (read == -1) {
// we've reached the end of the file.
if (buffer.position() == 0) return null;
return new String(buffer, 0, buffer.position(), charset);
}
}
}
}
为了效率,这段代码不能处理长度超过1024的日志行;随意增加这个数字。如果您希望能够读取无限大小的日志线,那么在某些时候巨大的缓冲区是一个问题。如果必须,您可以编写代码在达到 1024 时调整缓冲区大小,或者您可以更新此代码以使其继续读取,但仅返回前 1024 个字符的截断字符串。我会把它留给你做练习。
注意:我也没有对此进行测试,但至少它应该为您提供使用 SeekableByteChannel 的一般要点,以及缓冲区的概念。
使用方法:
Path p = Paths.get("D://logfile.txt");
try (LogLineReader reader = new LogLineReader(p)) {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
// do something with line
}
}
您必须确保 LLR 对象已关闭,因此,请使用 try-with-resources。