Java io 字节输入输出流

发布于 2024-06-12 05:53:45 字数 8179 浏览 24 评论 0

InputStream 代表输入字节流,OutputStream 代表输出字节流,如果读写的是文本文件则可使用 Reader 和 Writer 表示字符流,字符流传输的最小数据单位是 char,字符类型 char 表示一个字符。Java 的 char 类型除了可表示标准的 ASCII 外,还可以表示一个 Unicode 字符。

IO

InputStream

InputStream

InputStream 分为两类:

  • 直接提供数据的基础 InputStream
    • FileInputStream
    • ByteArrayInputStream
    • ServletInputStream
  • 提供额外附加功能的 InputStream(继承自 FilterInputStream)
    • BufferedInputStream:提供缓冲的功能来提高读取的效率, 默认缓冲区大小是 8M
    • DigestInputStream: 添加计算签名的功能
    • CipherInputStream: 添加加密/解密功能
//数据来源自文件
InputStream file = new FileInputStream("test.gz");

//提供缓冲的功能来提高读取的效率
InputStream buffered = new BufferedInputStream(file);

//假设该文件已经用 gzip 压缩了,我们希望直接读取解压缩的内容
InputStream gzip = new GZIPInputStream(buffered);

这里用到了装饰器模式:Decorator, 可以让我们通过少量的类来实现各种功能的组合

FileInputStream

从文件流中读取数据

File 相关概念解析

  • . 表示当前目录,.. 表示上级目录
  • getPath(),返回构造方法传入的路径
  • getAbsolutePath(),返回绝对路径,
  • getCanonicalPath,它和绝对路径类似,但是返回的是规范路径

绝对路径可以表示成 C:\Windows\System32..\notepad.exe,而规范路径就是把.和..转换成标准的绝对路径后的路径:C:\Windows\notepad.exe

read()

读取输入流的下一个字节,并返回字节表示的 int 值(0~255)。如果已读到末尾,返回-1 表示不能继续读取了

public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入 finally 并调用 close()
}

计算机内存的最小存储单元是字节(byte),一个字节就是一个 8 位二进制数,即 8 个 bit。它的二进制表示范围从 0000000011111111,换算成十进制是 0255,换算成十六进制是 00~ff, 二进制文件不利于直观查看,可以转成常用的十进制进行展示,因此需要把读取的字节从二进制转成十进制整数,故返回 int 型

read(byte[] bytes)

此方法是利用缓冲区一次性读取多个字节,效率比一个字节一个字节读取效率要高很多

返回值不再是字节的 int 值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了

StringBuilder sb = new StringBuilder();
try(InputStream inputStream = new FileInputStream("src/readme.txt")) {
byte[] bytes = new byte[1024];
int n;
while ((n = inputStream.read(bytes)) > -1) {
//n 表示读取了多少字节
System.out.println(n);
sb.append(new String(bytes));
}
System.out.println("读取到的: " + sb.toString());
} catch (IOException e) {
e.printStackTrace();
}

比如某个文本文件大小是 3kb,设置的缓冲区 byte[] 大小为 1024 (即 1024 字节) 则 while 会执行 3 次,每次 n 返回 1024

transferTo(OutputStream out)

JDK 9 新增了 InputStream.transferTo(OutputStream) 方法,此方法允许从对象调用方法表示的输入流中轻松传输(复制)字节到提供给该方法的输出流。正如方法的 Javadoc 注释所述,从该输入流中读取所有字节,并按照读取的顺序将字节写入给定的输出流。

//通过 try-with-resources 语句可以确保两个资源的正确回收
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt"))
{
input.transferTo(output);
}catch(IOException e) {
e.printStackTrace();
}

注意这个方法和 spring 提供的不是同一个方法

OutputStream

OutputStream

FileOutputStream

write

写入字节到输出流。要注意的是,如果是 write(int) 传入的是 int 参数,但只会写入一个字节,即只写入 int 最低 8 位表示字节的部分(相当于 b & 0xff)

public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("out/readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入 finally 并调用 close()
}

flush

为什么要有 flush()?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个 byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多 IO 设备来说,一次写一个字节和一次写 1000 个字节,花费的时间几乎是完全一样的,所以 OutputStream 有个 flush() 方法,能强制把缓冲区内容输出。

通常情况下,我们不需要调用这个 flush() 方法,因为缓冲区写满了 OutputStream 会自动调用它,并且,在调用 close() 方法关闭 OutputStream 之前,也会自动调用 flush() 方法

但是,在某些情况下,我们必须手动调用 flush() 方法: 写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是 4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。 解决办法就是每输入一句话后,立刻调用 flush(),不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去

Reader

Reader

Reader 是 Java 的 IO 库提供的另一个输入流接口。和 InputStream 的区别是,InputStream 是一个字节流,即以 byte 为单位读取,而 Reader 是一个字符流,即以 char 为单位读取

StringReader 可以直接把 String 作为数据源: Reader reader = new StringReader(“Hello”)

FileReader

对读取文件操作系统的封装,所有的读写都是直接操作文件系统。因此如果是频繁读写操作,不建议使用 FileReader 和 FileWriter,性能将会非常低,这时你需要使用BufferedReader

BufferedReader 在读取文本文件时,会先尽量从文件中读入字符数据并置入缓冲区,而之后若使用 read() 方法,会先从缓冲区中进行读取。如果缓冲区数据不足,才会再从文件中读取,使用 BufferedWriter 时,写入的数据并不会先输出到目的地,而是先存储至缓冲区中。如果缓冲区中的数据满了,才会一次对目的地进行写出

public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}

FileReader 默认的编码与系统相关,例如,Windows 系统的默认编码可能是 GBK,打开一个 UTF-8 编码的文本文件就会出现乱码。要避免乱码问题,需要在创建 FileReader 时指定编码

InputStreamReader

可以把任何 InputStream 转换为 Reader

try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), StandardCharsets.UTF_8)) {
// TODO:
}

Writer

Writer

把 char 转换为 byte 并输出, 比 OutputStream 多了一个写入 String 的方法:void write(String s)

FileWriter

try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入 char[]
writer.write("Hello"); // 写入 String
}

OutputStreamWriter

将任意的 OutputStream 转换为 Writer 的转换器

try (Writer writer = new OutputStreamWriter(new FileOutputStream(“readme.txt”), StandardCharsets.UTF_8)) {
// TODO:
}

RandomAccessFile

RandomAccessFile 虽然属于 java.io 下的类,但它不是 InputStream 或者 OutputStream 的子类;它也不同于 FileInputStream 和 FileOutputStream。提供了对文件的读写功能

RandomAccessFile 的基本功能有:定位用的 getFilePointer(),在文件里移动用的 seek(),以及判断文件大小的 length()、skipBytes() 跳过多少字节数。此外,它的构造函数还要一个表示以只读方式(“r”),还是以读写方式(“rw”) 打开文件的参数。实际它和 C 的 fopen() 一模一样,都是直接对文件句柄操作。

构造函数中 mode 参数传值介绍:

  • r 代表以只读方式打开指定文件 。
  • rw 以读写方式打开指定文件 。
  • rws 读写方式打开,并对内容或元数据都同步写入底层存储设备 。
  • rwd 读写方式打开,对文件内容的更新同步更新至底层存储设备 。
//读指定文件的内容,并且输出控制台
RandomAccessFile raf=new RandomAccessFile("G:\\java-lambda\\work.txt","r");
byte[] buff = new byte[1024];
int len = 0;
while ((len = raf.read(buff,0,1024))!=-1){
System.out.println(new String(buff,0,len));
}

Files

从 Java 7 开始,提供了 Files 和 Paths 这两个工具类,能极大地方便我们读写文件

//把一个文件的全部内容读取为一个 byte[]
byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));

// 默认使用 UTF-8 编码读取:
String content1 = Files.readString(Paths.get("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));


// 写入二进制文件:
byte[] data = ...
Files.write(Paths.get("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Paths.get("/path/to/file.txt"), lines);

//还有 copy()、delete()、exists()、move() 等快捷方法操作文件和目录

Files 提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个 G 的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容

对于大一些的流,为了提高效率就会用到 nio 的相关知识了,而 nio 比较复杂,一般都会基于 netty 来做二次封装

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

古镇旧梦

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

新人笑

文章 0 评论 0

mb_vYjKhcd3

文章 0 评论 0

小高

文章 0 评论 0

来日方长

文章 0 评论 0

哄哄

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文