☰
仓颉 std.io 实战:BufferedOutputStream 缓冲输出流的构造、写入与资源管理
2026/10/15 8:50:55 网站建设 项目流程
  • 人工智能
  • AI 技能
  • 文档
  • 开发工具

【免费下载链接】DocAgent

项目地址:https://gitcode.com/Cangjie-SIG/DocAgent
点击查看免费下载

BufferedOutputStream<T>是仓颉标准库 std.io 中提供带缓冲区的输出流实现,它包装任意OutputStream类型的底层输出流(如ByteBuffer),先将写入数据暂存于内部缓冲区,再成批地刷入目标流,从而减少底层 I/O 调用次数。本文将以 BufferedOutputStream 类文档 为主线,结合 io_package_classes.md、io_samples/buffered_output_stream.md 等仓库文档,完整讲解其三个构造函数、写入/刷新/重置等核心成员函数,以及Resource与Seekable两个扩展接口的使用方法,帮助读者掌握缓冲输出流在实际项目中的正确打开、写入、刷新与关闭方式。

一、类定位:什么是缓冲输出流

在 std.io 包概览 中,I/O 操作被描述为“程序与外部设备进行数据交换的操作”,输入输出流类似一条承载有序数据的数据通道:程序从输入流读取数据(来自文件、网络等),向输出流写入数据(通往文件、网络等)。BufferedOutputStream 类文档 将该类定位为“提供带缓冲区的输出流”:

可将其他OutputStream类型的输入流(如ByteBuffer)绑定到BufferedOutputStream实例,从该实例写入数据时,先把数据写入缓冲区暂存,再从缓冲区写入数据到流中。

从 io_package_classes.md 可以看到完整的类声明:

public class BufferedOutputStream<T> <: OutputStream where T <: OutputStream { public init(output: T) public init(output: T, buffer: Array<Byte>) public init(output: T, capacity: Int64) }

关键设计点:

  • 泛型约束:T <: OutputStream表示被绑定的底层输出流必须实现 OutputStream 接口,该接口要求实现write(buffer: Array<Byte>): Unit,并提供默认空实现的flush(): Unit。
  • 父类型:BufferedOutputStream<T>自身同样继承自OutputStream,因此它可以被嵌套包装,也可以传递给任何接受OutputStream的 API(例如 copy 函数)。
  • 缓冲语义:写入的数据先进入内部缓冲区,只有调用flush()时才会真正落入被绑定的底层流。这与 BufferedInputStream 形成对称的读/写缓冲结构。

二、构造函数:三种方式配置内部缓冲区

构造函数是使用BufferedOutputStream的第一步,类文档 给出了三种重载:

名称原型说明
init(output: T)创建实例,缓冲区容量取默认值 4096。
init(output: T, buffer: Array<Byte>)内部使用的缓冲区由入参决定;注重性能的场景下,通过复用传入的buffer可减少内存分配次数、提高性能。
init(output: T, capacity: Int64)按指定容量创建实例。

2.1 默认构造:init(output: T)

不指定缓冲区容量,内部缓冲区大小为默认值4096 字节。适用于大多数普通场景,无需关心内部细节:

import std.io.ByteBuffer import std.io.BufferedOutputStream import std.io.readToEnd main(): Unit { let outputStream = ByteBuffer() /* 绑定指定输出流 */ let bufferedStream = BufferedOutputStream(outputStream) /* 将输出数据写入缓冲流 bufferedStream 并刷新内部绑定的输出流 outputStream */ let outputData = "Hello World".toArray() bufferedStream.write(outputData) bufferedStream.flush() println(String.fromUtf8(readToEnd(outputStream))) }

运行结果:

Hello World

该示例可在 io_package_classes.md 的init(T)小节找到,其中readToEnd是 std.io 提供的读取函数,用于取走ByteBuffer中未被读取的全部数据。

2.2 复用缓冲区:init(output: T, buffer: Array<Byte>)

在需要反复创建缓冲流的高频场景(如循环写入大量小块数据),可以通过传入预先分配的Array<Byte>来复用内存,避免每次构造都重新分配缓冲区。仓库文档明确指出这是“注重性能的场景”下的推荐做法。

需要注意:当buffer大小等于 0 时,会抛出 IllegalArgumentException,对应错误信息为The buffer cannot be empty.:

import std.io.ByteBuffer import std.io.BufferedOutputStream main(): Unit { let outputStream = ByteBuffer() /* 使用合法的内部缓冲区创建实例,不会抛出异常 */ try { let buffer = Array<Byte>(1024, repeat: 0) let bufferedStream = BufferedOutputStream(outputStream, buffer) } catch (e: IllegalArgumentException) { println("Error: ${e.message}") } /* 内部缓冲区大小被定义为0的情况 */ try { let invalidBuffer = Array<Byte>() let bufferedStream = BufferedOutputStream(outputStream, invalidBuffer) } catch (e: IllegalArgumentException) { println("Error: ${e.message}") } }

运行结果:

Error: The buffer cannot be empty.

2.3 指定容量:init(output: T, capacity: Int64)

当业务明确知道单次写入的数据规模时,可以按需设定缓冲区容量。当capacity小于等于 0 时,同样抛出IllegalArgumentException:

import std.io.ByteBuffer import std.io.BufferedOutputStream main(): Unit { let outputStream = ByteBuffer() /* 使用合法的内部缓冲区容量创建实例,不会抛出异常 */ try { let capacity = 2048 let bufferedStream = BufferedOutputStream(outputStream, capacity) } catch (e: IllegalArgumentException) { println("Error: ${e.message}") } /* 当内部缓冲区被设置成 0 时,抛出异常 */ try { let zeroCapacity = 0 let bufferedStream = BufferedOutputStream(outputStream, zeroCapacity) } catch (e: IllegalArgumentException) { println("Error: ${e.message}") } /* 当内部缓冲区被设置成负数时,抛出异常 */ try { let negativeCapacity = -1024 let bufferedStream = BufferedOutputStream(outputStream, negativeCapacity) } catch (e: IllegalArgumentException) { println("Error: ${e.message}") } }

运行结果:

Error: Invalid capacity size: capacity = 0. Error: Invalid capacity size: capacity = -1024.

三、成员函数:写入、刷新与重置

类文档 定义了 4 个成员函数:

名称原型说明
flush() -> Unit将内部缓冲区的剩余数据写入绑定的输出流,并刷新 BufferedOutputStream。
reset(output: T) -> Unit绑定新的输出流,重置状态,但不重置capacity。
write(buffer: Array<Byte>) -> Unit将buffer中的数据写入到绑定的输出流中。
writeByte(v: Byte) -> Unit写入一个字节到绑定的输出流中。

3.1 write:批量写入数据

write(buffer: Array<Byte>)接受一个字节数组作为待写入数据。注意其语义是“写入到绑定的输出流”,但数据实际先进入内部缓冲区暂存,并不会立刻到达底层流。典型用法如下(与 flush 配套):

import std.io.ByteBuffer import std.io.BufferedOutputStream import std.io.readToEnd main(): Unit { let outputStream = ByteBuffer() /* 绑定指定输出流 */ let bufferedStream = BufferedOutputStream(outputStream) /* 将输出数据写入缓冲流 bufferedStream 并刷新内部绑定的输出流 outputStream */ let outputData = "Hello World".toArray() bufferedStream.write(outputData) bufferedStream.flush() println(String.fromUtf8(readToEnd(outputStream))) }

3.2 writeByte:逐字节写入

对于需要逐字节构造数据的场景,writeByte(v: Byte)可一次写入一个字节。多次调用后缓冲区会积攒数据,最终仍需flush()落盘:

import std.io.ByteBuffer import std.io.BufferedOutputStream import std.io.readToEnd main(): Unit { let outputStream = ByteBuffer() /* 绑定指定输出流 */ let bufferedStream = BufferedOutputStream(outputStream) /* 将输出数据逐个写入缓冲流 bufferedStream 并刷新内部绑定的输出流 outputStream */ let outputData = "Hello World".toArray() for (byte in outputData){ bufferedStream.writeByte(byte) } bufferedStream.flush() println(String.fromUtf8(readToEnd(outputStream))) }

运行结果:

Hello World

3.3 flush:真正落盘的关键

缓冲的意义在于“攒批”,因此flush()是数据可见性的关键:它“将内部缓冲区的剩余数据写入绑定的输出流,并刷新 BufferedOutputStream”。在 io_samples/buffered_output_stream.md 的官方示例中,这一“先写缓冲区、再 flush 落盘”的过程有非常直观的注释:

import std.io.* main(): Unit { let arr1 = "01234".toArray() let byteBuffer = ByteBuffer() byteBuffer.write(arr1) let bufferedOutputStream = BufferedOutputStream(byteBuffer) let arr2 = "56789".toArray() /* 向流中写入数据,此时数据在外部流的缓冲区中 */ bufferedOutputStream.write(arr2) /* 调用 flush 函数,真正将数据写入内部流中 */ bufferedOutputStream.flush() println(String.fromUtf8(readToEnd(byteBuffer))) }

运行结果:

0123456789

从底层看,OutputStream 接口 的flush()默认实现为空,而BufferedOutputStream重写了该语义——先冲刷自身内部缓冲区到被绑定的流,再调用底层流的flush(),因此任何继承自OutputStream的实现(如ByteBuffer、文件流等)都能获得一致的“先攒后写”行为。

3.4 reset:切换绑定的输出流

reset(output: T)允许为同一个缓冲对象绑定新的输出流,并重置内部状态,但不会重置capacity——即缓冲区容量在实例生命周期内保持稳定,适合需要复用一个缓冲流对象连续写多个目标的场景:

import std.io.ByteBuffer import std.io.BufferedOutputStream import std.io.IOException import std.io.readToEnd main(): Unit { /* 创建第一个输出流 */ let outputStream1 = ByteBuffer() let sourceData1 = "First message: Hello".toArray() /* 创建第二个输出流 */ let outputStream2 = ByteBuffer() let sourceData2 = "Second message: World".toArray() /* 使用 BufferedOutputStream 包装第一个输出流 */ let bufferedStream = BufferedOutputStream(outputStream1) /* 将第一个源数据写入到绑定的第一个输出流中并刷新 */ bufferedStream.write(sourceData1) bufferedStream.flush() println(String.fromUtf8(readToEnd(outputStream1))) /* 重置输出流为第二个输出流,将第二个源数据写入到绑定的第二个输出流中并刷新 */ bufferedStream.reset(outputStream2) bufferedStream.write(sourceData2) bufferedStream.flush() println(String.fromUtf8(readToEnd(outputStream2))) }

运行结果:

First message: Hello Second message: World

四、扩展接口:Resource 与自动资源释放

当被绑定的底层流同样实现了Resource接口时,BufferedOutputStream<T>会通过扩展接口BufferedOutputStream<T> <: Resource where T <: Resource获得资源管理能力,可在try-with-resource语法上下文中实现自动资源释放(详见 io_package_classes.md)。

扩展接口新增两个成员:

名称原型说明
close() -> Unit关闭当前流。注意:调用此方法后不可再调用 BufferedOutputStream 的其他接口,否则会造成不可期现象。
isClosed() -> Bool判断当前流是否关闭。

完整示例(自定义同时实现OutputStream与Resource的类):

import std.io.BufferedOutputStream import std.io.OutputStream import std.io.ByteBuffer import std.io.readToEnd /** * 自定义实现 OutputStream 和 Resource 接口的类A */ public class A <: OutputStream & Resource { private var closed: Bool = false public var outputStream = ByteBuffer() public func write(buffer: Array<Byte>): Unit { this.outputStream = ByteBuffer(buffer) } public func isClosed(): Bool { return closed } public func close(): Unit { println("Resource is closed") closed = true } } main(): Unit { let resourceStream = A() let bufferedStream = BufferedOutputStream(resourceStream) /* 使用try-with-resource语法获取资源 */ try (r = bufferedStream) { println("Get the resource") let data = "Hello World".toArray() r.write(data) r.flush() println(r.isClosed()) println(String.fromUtf8(readToEnd(resourceStream.outputStream))) } /* 自动调用 close() 函数释放资源 */ println(bufferedStream.isClosed()) }

运行结果:

Get the resource false Hello World Resource is closed true

这段输出清晰地展示了try-with-resource的执行时序:块内isClosed()为false;块正常结束时会自动调用close()释放资源(打印Resource is closed),块外再次检查isClosed()返回true。这意味着在实际项目中,用try (r = BufferedOutputStream(...)) { ... }包裹写入逻辑即可免去手动关闭的负担。

五、扩展接口:Seekable 与光标移动

当被绑定的底层流实现Seekable接口时,BufferedOutputStream<T> <: Seekable where T <: Seekable扩展使其具备查询数据长度、移动光标的能力,这与 Seekable 接口定义 保持一致:

名称原型说明
prop lengthInt64返回当前流中的总数据量。
prop positionInt64返回当前光标位置。
prop remainLengthInt64返回当前流中未读的数据量。
seek(sp: SeekPosition) -> Int64移动光标到指定的位置。

seek的使用有三个重要约定(来自类文档与 io_package_classes.md):

  • 指定的位置不能位于流中数据头部之前,否则抛出 IOException(错误信息为Can't move the position before the beginning of the stream.);
  • 指定位置可以超过流中数据末尾,属于合法操作;
  • 调用该函数会先将缓存区内的数据写到绑定的输出流里,再移动光标的位置,保证底层流数据完整后再定位。

SeekPosition是 std.io 中的枚举类型,包含三种定位基准:

public enum SeekPosition { | Begin(Int64) // 从起点开始移动 | Current(Int64) // 从当前位置开始移动 | End(Int64) // 从末尾开始移动 }

完整示例(自定义同时实现OutputStream与Seekable的类):

import std.io.BufferedOutputStream import std.io.OutputStream import std.io.ByteBuffer import std.io.Seekable import std.io.SeekPosition import std.io.IOException import std.io.readToEnd /** * 自定义实现 OutputStream 和 Seekable 接口的类A */ public class A <: OutputStream & Seekable { public var outputStream: ByteBuffer = ByteBuffer() public func write(buffer: Array<Byte>): Unit { this.outputStream = ByteBuffer(buffer) } public func seek(sp: SeekPosition): Int64 { return outputStream.seek(sp) } } main(): Unit { let seekableStream = A() let bufferedStream = BufferedOutputStream(seekableStream) let data = "Hello World".toArray() bufferedStream.write(data) bufferedStream.flush() /* 输出当前流中总数据量,当前光标位置,当前流中未读的数据量 */ println("Length : " + bufferedStream.length.toString()) println("Position : " + bufferedStream.position.toString()) println("Remain Length : " + bufferedStream.remainLength.toString()) /* 移动光标到指定位置,虽然超过了流中数据末尾但是合法的 */ println("Position after seek() : " + bufferedStream.seek(SeekPosition.Current(11)).toString()) /* 尝试移动到数据头部之前,抛出异常 */ try { bufferedStream.seek(SeekPosition.Begin(-1)) } catch (e: IOException) { println("Error: " + e.message) } /* 将光标移动到第一个单词之后,读取后续的数据 */ bufferedStream.seek(SeekPosition.Begin(6)) println(String.fromUtf8(readToEnd(seekableStream.outputStream))) }

运行结果:

Length : 11 Position : 0 Remain Length : 11 Position after seek() : 11 Error: Can't move the position before the beginning of the stream. World

注意运行结果中的细节:写入并flush()后,光标位置为 0、未读数据量为 11;seek(SeekPosition.Current(11))返回 11(起点到移动后位置的字节偏移);SeekPosition.Begin(-1)因越界抛出IOException;最后将光标移到偏移 6 处后,readToEnd读出剩余的World,说明光标移动已成功作用到底层流。

六、异常与使用注意事项总结

综合 类文档 与 io_package_classes.md 中的可验证信息,使用BufferedOutputStream时需特别注意:

  1. 缓冲区参数校验:buffer大小等于 0 或capacity小于等于 0 时,构造均抛出IllegalArgumentException(对应The buffer cannot be empty.与Invalid capacity size: capacity = 0.等错误信息)。
  2. 关闭后不可再用:close()之后调用其他接口会造成不可期现象,应通过isClosed()或try-with-resource结构规避。
  3. seek 越界:光标不允许移到流数据头部之前,否则抛出IOException;但允许超过数据末尾。
  4. 缓冲数据不可见性:写入的数据在flush()之前只存在于内部缓冲区,若中途不再写入,务必在读取侧验证前调用flush()。
  5. 能力取决于底层流:Resource与Seekable扩展均是条件扩展(where T <: Resource/where T <: Seekable),只有当绑定的T实现对应接口时才可用,这也是选择底层流类型时需要预先考虑的因素。

七、小结

BufferedOutputStream<T>是仓颉标准库中实现“写缓冲”的核心类,通过三层结构支撑了日常 I/O 开发:

  • 构造层:默认 4096 字节容量、复用外部缓冲区、显式指定容量三种方式,兼顾易用与性能;
  • 功能层:write/writeByte负责攒数据,flush负责落盘,reset支持复用对象切换目标流;
  • 能力层:条件扩展Resource提供try-with-resource自动释放,条件扩展Seekable提供length/position/remainLength/seek随机访问能力。

官方示例 buffered_output_stream.md 以及 ByteBuffer 示例 中的write、seek、readToEnd、readString等用法,均可与本文内容组合使用;更多接口级细节可继续查阅 io_package_api/io_package_classes.md、io_package_api/io_package_interfaces.md 与 io_package_api/io_package_funcs.md。

  • 人工智能
  • AI 技能
  • 文档
  • 开发工具

【免费下载链接】DocAgent

项目地址:https://gitcode.com/Cangjie-SIG/DocAgent
点击查看免费下载

相关推荐

上一篇:终极游戏模组管理神器:Reloaded-II完整使用指南
下一篇:如何让老Mac焕发新生:OpenCore Legacy Patcher终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询