【问题标题】:Why is java.io.Reader#skip implemented the way that it is?为什么 java.io.Reader#skip 是这样实现的?
【发布时间】:2011-09-07 08:42:09
【问题描述】:

我仍在学习 Java 中的面向对象编程。我正在查看java.io.Reader.skip 的Java 实现,我想知道为什么它是这样实现的。特别是我对我注意到的这些事情有疑问:

  1. skip(long) 使用的缓冲区是 Reader 对象的一个​​字段,而不是方法中的普通变量。
  2. 最大缓冲区长度远小于Integer.MAX_VALUE 2147,483,647。特别是,Java 的实现使用了 8192。
  3. java.io.InputStream 以完全相同的方式实现跳过。

现在,我个人认为缓冲区是一个字段的原因是,缓冲区不必因为反复重新初始化而反复被垃圾收集。这可能会加快跳跃速度。

我认为缓冲区长度变小与使 Reader 阻塞的时间更短有关,但由于 Reader 是同步的,这真的会有所不同吗?

字节流以相同的方式实现它,可能是为了保持一致性。我对这三件事的假设是否正确?

总而言之,我的问题是:对于字符数组,使用字段而不是变量平均会产生多大的速度差异?使用Integer.MAX_VALUE 作为最大缓冲区长度不是一样吗?因为其他read 方法只调用无参数read,所以在for 循环中为字节流使用无参数read 方法不是更好更容易吗?

对不起,如果我的问题是一个奇怪的问题,但我认为通过这个问题我可以学到很多关于面向对象编程的知识。

【问题讨论】:

  • 较小缓冲区的原因是为了减少消耗的内存量。如果缓冲区为 2 GB,它会将 2 GB 读入内存,然后将其刷新到流写入的任何位置,而不是像 8K 这样更小的缓冲区。
  • @vcsjones 我明白了 =) 这确实有道理。非常感谢。这就解释了缓冲区大小正好是 8k。我必须记住,一个字符在内存中消耗一个字节,但我想我还在学习^_^;

标签: java stream character java-io


【解决方案1】:

对于InputStream,您通常具有允许更有效地跳过的子类,并且这些子类会适当地覆盖skip 方法。但是对于那些没有有效跳过方式的子类(比如压缩或解压输入流),skip方法是基于读取实现的,所以不是每个子类都必须这样做。

java.io 包中有几种关于如何实现这一点的策略:

跳过基本流:

  • FilterInputStream.skip() 只是委托给源流。不过,我不太确定这有多大用处。

  • DataInputStream 不会覆盖skip(),但有另一个名为skipBytes() 的方法可以做同样的事情(不过,仅适用于int 参数)。它委托给底层源流。

  • BufferedInputStream.skip() 覆盖它,首先跳过它自己的缓冲区中的现有内容,然后在基本流上调用skip()(如果没有设置mark() - 如果有标记,它必须将所有内容读入缓冲区以支持reset())。

  • PushbackInputStream.skip() 首先跳过其推回缓冲区,然后调用super.skip()(即FilterInputStream.skip(),见上文)。

重置索引:

  • ByteArrayInputStream 可以轻松支持跳过,只需设置接下来要阅读的位置即可。

  • StringBufferInputStream(这是 StringReader 的弃用版本)支持只需重置索引即可跳过。

原生魔法:

  • FileInputStream 具有 skip() 作为 native 方法。我认为这将是最有用的典型示例。

阅读所有内容并将其扔掉:

  • LineNumberInputStream.skip() 必须阅读所有内容才能计算行数。 (我不知道这个类存在。改用LineNumberReader。)

  • ObjectInputStream 不会覆盖skip(),但有另一个名为skipBytes() 的方法可以做同样的事情(不过,仅适用于int 参数)。它委托给一个内部类 (BlockDataInputStream.skip()),后者依次从底层流中读取数据,遵守块数据的对象流协议。

InputStream 中的默认实现:

  • SequenceInputStreamPipedInputStream 也使用它。

让我们看看Reader 类。原则上,同样的策略适用:

跳过基本阅读器/流:

  • FilterReader.skip() 会这样做。

  • PushBackReader 首先跳过它自己的推回缓冲区,然后是基本阅读器。

重置一些索引:

  • StringReader(这个其实支持后跳)

  • CharArrayReader

阅读所有内容并将其扔掉:

  • 默认Reader.skip()PipedReader也使用。

  • 对于InputStreamReader“简单地跳过基本流” 方法仅适用于固定字节数字符集(即ISO-8859 系列、UTF-16 和一些类似的个),不适用于UTF-8UTF-32 或其他每个字符的字节数可变的字符集,因为实际上我们必须读取所有字节才能知道它们代表多少个字符。这也适用于它的子类FileReader

  • BufferedReader(它不调用自己的read(),而是填充其内部缓冲区,该缓冲区从基本流中读取)。

  • LineNumberReader(它必须这样做才能跟踪行号)

【讨论】:

  • 我明白了 =) 我想我必须记住,最重要的是,java.io.InputStreamjava.io.Reader 只是抽象类,更专业的类将覆盖 skip 方法以提供更多有效或适当的行为。
【解决方案2】:

对于字符数组,使用字段而不是变量平均会产生多大的速度差异?

这肯定会因 JVM 而异,但重复分配 8K 数组可能不如保留一个数组便宜。当然,这里隐藏的教训是,不应该抓住读者,即使是封闭的读者,因为它们会受到 8K 的惩罚。

使用 Integer.MAX_VALUE 作为最大缓冲区长度不一样吗?

缓冲区必须预先分配,分配 2Gb 数组似乎有点过头了。请记住,分页的原因是为了分摊读取调用的成本——这有时会变成本机操作。

在for循环中对字节流使用无参数读取方法不是更好更容易吗,因为其他读取方法只是调用无参数读取?

不能保证底层流被缓冲,因此这可能会导致每次调用的大量开销。

最后,请记住,java.io 类有很多很多不足之处,所以不要认为那里的一切都是有充分理由的。

【讨论】:

  • 好奇 java.io 类中的“很多很多缺陷”是什么?
  • 从非最终受保护成员开始,其对线程的访问模型完全未定义。使用魔术 -1 而不是公共常量。当 Java 1.1 出现时,为时已晚。公共 API 被冲洗掉了。
  • 我假设您的意思是 read() 方法的“-1”?好吧,没有什么能阻止他们在任何时间点保持不变,但我不认为这是“缺陷”。我确实同意受保护的成员有点丑陋并且没有经过深思熟虑,但我很少需要以这样的方式处理 io 类,这是一个主要问题。对于“普通”开发人员,我会说 io 包实际上状态非常好。当然不属于“很多很多缺陷”的范畴。 (我当然接受 java 有它的缺点,这对我来说似乎相当小)。
  • (特别是考虑到有问题的skip() 实现似乎相当合理)。
  • @jtahborn -- Java 是我最喜欢的语言之一,但我认为我们必须同意不同意“对于普通开发人员来说,我会说 io 包实际上相当不错形状”。我的案例和观点——读取文件内容和关闭流的正确习惯用法是什么?
【解决方案3】:

您忘记了 2^31 - 1 的缓冲区是必须分配的 2 GB 内存,然后不能用于其他任何事情

分配一个 2 千兆字节的大连续字节块对于读取字节来说是多余的,并且可能导致内存不足的情况

8 kB 的最大内存缓冲区是更好的选择和更好的权衡,因为它只会分配一次(并且将在每个跳过操作中重复使用)

java.io.InputStream 中顺便说一句,skipbuff 是静态的,只分配一次,但由于没有读取它(它只是用作只写内存),因此无需担心竞争

【讨论】:

  • 是的,你是对的。我忘记了每个字符至少有一个字节。所以 8 kB(或类似的)缓冲区要好得多。顺便说一句,我不确定你所说的种族是什么意思,但我想你在说skipbuff是静态的时可能打错了。至少在我看过的实现中不是这样。我什至考虑过问为什么不这样做,我推断这是因为如果您有多个使用字节流的线程,您必须在其上进行同步,这会显着减慢跳过速度。我可能错了。
  • @user check docjar.com/html/api/java/io/InputStream.java.htmlskipBuf 用于跳过,它是静态的,因为如果你只写(使用缓冲区作为废内存),你可以共享缓冲区而不会产生任何后果
  • 嗯,其实我说的是字符输入流 Reader,但你说得对,字节输入流确实使用了静态缓冲区和本地副本,这就引出了为什么 Reader 不使用的问题也这样做,因为就像你说的那样,它只用于废记忆。但是感谢您解释为什么它是可共享的。我对此有点困惑,因为它被传递给了read(byte[], int, int) 方法,但现在我不是 =)
【解决方案4】:

一次读取一个字符的效率会低得多 - 每个字节跳过一个方法调用,这通常不利于大跳过(大量开销)。

暂存缓冲区大小很容易回答:如果您要从文件中跳过 2G,您真的要分配 Integer.MAX_VALUERAM 吗?

至于确切的大小,以及是否使用实例变量,这是一个依赖于实现的折衷方案。您正在阅读选择 8192 成员的实现。一些实现具有较小的本地实现(可以看到 512 here)。

标准中没有任何内容需要这些实现细节,所以根本不要依赖它们。

如果您打算做类似的事情,基准不同的方法,并根据您的具体情况选择最佳折衷方案。

【讨论】:

  • 我理解,你说得对,标准没有指定任何这些细节,所以我想不同的实现可能使用不同的缓冲区大小,这可能解释了最大缓冲区长度字段的原因也是一个字段而不是一个变量。 (为了允许隐藏字段,但在我看来,受保护的方法会更好,因为我反对隐藏字段。)谢谢你的基准测试建议,我将尝试查找如何对我的程序进行基准测试=)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多