Java IO

Java 输入与输出

输入/输出流

概述

  • 输入流:可以从中读取一个字节序列的对象
  • 输出流:可以向其中写入一个字节序列的对象

其中,字节序列的来源或者目的地可以是:

  1. 文件(大多数情况下指文件)
  2. 网络连接
  3. 内存块

抽象类 InputStream 和 OutputStream 构成了输入/输出(I/O)类层次结构的基础

读写字节

InputStream 类有一个抽象方法 abstract int read(), 这个方法读入一个字节,并返回读入的字节,或者在遇到输入源结尾时返回 -1.

在设计具体的输入流类时,必须覆盖这个方法以提供适用的功能。

OutputStream 类有一个抽象方法 abstract void write(), 这个方法可以向某个输出位置写出一个字节。

read 和 write 方法在执行时都将阻塞,直至字节确实被读入或者写出。这就意味着如果流不能被立即访问(通常因为网络连接忙),那么当前线程将被阻塞。

available 方法可以检查当前可读入的字节数量,这意味着像下面这样的代码片段就不可能被阻塞:

1
2
3
4
5
int bytesAvailable = in.available();
if (bytesAvailable > 0) {
byte[] data = new byte[bytesAvailable];
in.read(data);
}

完成对输入/输出流的读写后,应该调用 close 方法关闭输入/输出流,close 方法会释放掉占用的操作系统资源。
如果一个应用程序打开了过多的输入/输出流而没有关闭,系统资源将被耗尽。
关闭一个输出流的同时还会自动冲刷该输出流的缓冲区:所有被临时置于缓冲区中,以便用更大的包的形式传递的字节在关闭输出流时都将被送出。
如果不关闭文件,那么写出的最后一个包可能永远得不到传递。当然,也可以使用 flush 方法手动冲刷缓冲区,但不关闭流总之不是个好习惯。

即使某个输入/输出流类实现了抽象类 InputStream/OutputStream 中的 read/write 抽象方法,但应用软件的程序员也很少使用,因为大家跟感兴趣的不是对于字节的读取/写入,而是对数字、字符串和对象的读取/写入。
所以我们可以使用众多的从基本的 InputStream/OutputStream 类派生出的子类中更实用的方法。

不过在这里我们还是先对 InputStream/OutputStream 类的方法做一个总结:

java.io.InputStream 1.8

修饰符和类型 方法名和描述
int available()
返回一个字节数,指在不被方法的下一个调用阻塞的情况下,输入流中能被读取(或者跳过)的字节数估计值。
void close()
关闭当前输入流并释放任何与当前输入流相关联的系统资源。
void mark(int readlimit)
在输入流的当前位置打一个标记(并非所有的流都支持这个特性)。
如果从输入流中已经读入的字节多于 readlimit个,则这个流允许忽略这个标记。
boolean markSupported()
如果这个流支持 mark 和 reset 方法,则返回 true,否则返回 false。
abstract int read()
从输入流中读取下一个字节,并返回该字节。这个read方法在碰到输入流的结尾时返回 -1。
int read(byte[] b)
从输入流中读取一些字节,并存到数组 b 中,返回实际读到的字节数,或者在输入流的结尾时返回 -1。这个方法最多读入 b.length 个字节
int read(byte[] b, int off, int len)
从输入流中读取 len 个字节到数组 b 中偏移 off 的位置,返回实际读入的字节数,或者在碰到输入流的结尾时返回 -1。
参数:
b: 数据读入的字节数组
off: 第一个读入字节应该被放置的位置在 b 中的偏移量
len: 读入字节的最大数量
void reset()
将输入流的当前位置重置到最后一次调用 mark 标记的位置。
long skip(long n)
在输入流中跳过 n 个字节的数据。

java.io.OutputStream 1.8

修饰符和类型 方法名和描述
void close()
关闭输出流并释放任何与当前输出流相关的系统资源。
void flush()
冲刷当前输出流,强制发送当前所有缓冲区的数据到目的地。
void write(byte[] b)
将指定字节数组中的所有字节到写入到当前输出流中。
void write(byte[] b, int off, int len)
将指定字节数组中 off 位置开始且长度为 len 的字节到写入到当前输出流中。
abstract void write(int b)
向当前输出流中写入一个字节的数据

完整的流家族

字节流

图2-1 输入流和输出流的层次结构

InputStream 的子类结构

  • AudioInputStream
  • ByteArrayInputStream
  • FileInputStream
  • FilterInputStream
    • BufferedInputStream
    • CheckedInputStream
    • CipherInputStream
    • DataInputStream: 以二进制的形式写所有的基本 Java 类型
    • DeflaterInputStream
    • DigestInputStream
    • InflaterInputStream
      • GZIPInputStream
      • ZipInputStream: 以常见的 ZIP 压缩格式写文件
        • JarInputStream
    • LineNumberInputStream
    • ProgressMonitorInputStream
    • PushbackInputStream
  • ObjectInputStream
  • PipedInputStream
  • SequenceInputStream
  • StringBufferInputStream

OutputStream 的子类结构

  • ByteArrayOutputStream
  • FileOutputStream
  • FilterOutputStream
    • BufferedOutputStream
    • CheckedOutputStream
    • CipherOutputStream
    • DataOutputStream: 以二进制的形式写所有的基本 Java 类型
    • DeflaterOutputStream
      • GZIPOutputStream
      • ZipOutputStream: 以常见的 ZIP 压缩格式写文件
        • JarOutputStream
    • DigestOutputStream
    • InflaterOutputStream
    • PrintStream
  • ObjectOutputStream
  • PipedOutputStream

字符流

对于 Unicode 文本,可以使用抽象类 Reader 和 Writer 的子类。Reader 和 Writer 类的基本方法与 InputStream 和 Outputstream 中的方法类似

abstract int read() 方法返回一个 Unicode 码元(一个在0~65535之间的整数),或者在碰到文件结尾时返回 -1。
abstract void write(int c) 方法需要传递一个 Unicode 码元
Reader 和 writer 的层次结构
图2-2 Reader 和 Writer 的层次结构

附加接口

图2-3 Closeable、Flushable、Readable 和 Appendable 接口

  1. Closeable: 只有一个方法 void close() throws IOException 关闭这个 Closeable
  2. Flushable: 只有一个方法 void flush() throws IOException 冲刷这个 Flushable
  3. Readable: 只有一个方法 int read(java.nio.CharBuffer cb) throws IOException
    1. read 方法表示尝试向 cb 中读入其可持有数量的 char 值。返回读入的 char 值的数量,或者当从这个 Readable 中无法再获得更多的值时返回 -1。
    2. CharBuffer 类拥有按顺序和随机读写访问的方法,它表示一个内存中的缓冲区或者一个内存映像的文件。
  4. Appendable: 有三个用于添加单个字符和字符序列的方法
    1. Appendable append(CharSequence csq) throws IOException 向这个 Appendable 中追加给定的码元序列中的所有码元。
    2. Appendable append(CharSequence csq, int start, int end) throws IOException 向这个 Appendable 中追加给定的码元序列中 [start, end) 区间内的所有码元。
    3. Appendable append(char c) throws IOException 向这个 Appendable 中追加给定的码元。
    4. CharSequence 接口描述了一个 char 值序列的基本属性,String、CharBuffer、String Builder 和 StringBuffer 都实现了它。
      1. char charAt(int index) 返回给定索引处的码元
      2. int length 返回在这个序列中的码元数量
      3. CharSequence subSequence(int startIndex, int endIndex) 返回由区间 [startIndex, endIndex) 内的所有码元构成的 CharSequence
      4. String toString() 返回这个序列中所有码元构成的字符串

InputStream、OutputStream、Reader 和 Writer 都实现了 Closeable 接口,而 OutputStream 和 Writer 还实现了 Flushable 接口。

在流类的家族中,只有 Writer 实现了 Appendable 接口。

组合输入/输出流过滤器

FileInputStream 和 FileOutputStream 可以提供附着在一个磁盘文件上的输入流和输出流,而你只需要向其构造器提供文件名或者文件的完整路径名。例如:
FileInputStream fin = new FileInputStream("employee.dat"); .
这行代码可以查看在用户目录下名为 “employee.dat” 的文件。

注: 所有在 java.io 中的类都将相对路径名解释为以用户当前工作目录(java项目的根目录,或者假如是单个java文件那就是java文件本身所在的目录)开始,你可以通过调用 `System.getProperty("user.dir")` 来获取这个信息。 对于可移植的程序,应该使用程序所运行平台的文件分割符,可以用常量字符串 java.io.File.separator 获取。

FileInputStream 只支持在字节级别上的读写,没有任何读入数值类型的方法,而 DataInputStream 只能读入数值类型,无法从文件中获取数据。

Java 中使用了一种灵巧的机制:某些输入流(例如 FileInputStream 和由 URL 类的 openstream 方法返回的输入流)可以从文件和其他更外部的位置上获取字节,而其他输入流(例如 DataInputStream)可以将字节组装到更为有用的数据类型中。
因此,为了从文件中读入数字,首先需要创建一个 FileInputStream,然后将其传递给 DataInputStream 的构造器:

1
2
3
FileInputStream fin = new FileInputStream("employee.dat");
DataInputStream in = new DataInputStream(fin);
double x = in.readDouble();

再次查看上面的 字节流 的类层次结构图,可以看到 FilterInputStream 和 FileOutputStream 类的子类用于向处理字节的输入/输出流添加额外功能。

可以通过嵌套过滤器来添加多重功能。例如,输入流在默认情况下是不被缓冲区缓存的,也就是说,每个对 read 的调用都会请求操作系统再分发一个字节。相比之下,请求一个数据块并将其置于缓冲区中会显得更加高效。如果我们想使用缓冲机制,以及用于文件的数据输入方法,那么就需要使用下面这种相当复杂的构造器序列:

1
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("employee.data")));

当多个输入流连接在一起时,需要跟踪各个中介输入流(intermediate input stream)。例如,当读入输入时,你经常需要预览下一个字节,以了解它是否时你想要的值。Java 提供了用于此目的的 PushbackInputStream:
PushbackInputStream pbin = new PushbackInputStream(new BufferedInputStream(new FileInputStream("employee.dat")));

现在你可以预读下一个字节: int b = pbin.read();
并且在它并非你所期望的值时将其推回流中。if (b != '<') pbin.unread(b); .
但是读入和推回时可应用于可回推(pushback)输入流的仅有的方法。如果你希望能够预先浏览并且还可以读入数字,那么你就需要一个既是可回推输入流,又是一个数据输入流的引用。
DataInputStream din = new DataInputStream(pbin=new PushbackInputStream(new BufferedInputStream(new FileInputStream("employee.dat"))));
当然,在其他编程语言的输入/输出流类库中,诸如缓冲机制和预览等细节都是自动处理的。因此,相比较而言,Java 就有一些麻烦,它必须将多个流过滤器组合起来。
但是,这种混合并匹配过滤器类以构建真正有用的输入/输出流的能力,将带来极大的灵活性,例如,你可以从一个 ZIP 压缩文件中通过使用下面的输入流序列来读入数字:

1
2
ZipInputStream zin = new ZipInputStream(new FileInputStream("employee.zip"));
DataInputStream din = new DataInputStream(zin);

图2-4 过滤器流序列

文本输入与输出

保存数据时可以选择二进制格式或者文本格式。
二进制格式的 I/O 高速且高效,但不适合人来阅读,相对的,文本格式适合人来阅读,但 I/O 速度相对低一些。

读写文本格式

存储文本字符串时,需要考虑字符编码方式。在 Java 内部使用 UTF-16 的编码方式,字符串 “1234” 编码为 “00 31 00 32 00 33 00 34”。
UTF-8 的编码方式则在互联网上最为常用,字符串 “1234” 编码为 4A 6F 73 C3 A9

OutputStreamWriter 类将使用选定的字符编码方式,把 Unicode 码元的输出流转换为字节流。而 InputStreamReader 类则将包含字节(用某种字符编码方式表示的字符)的输入流转换为可以产生的 Unicode 码元的读入器。
例如,下面的代码可以让一个输入读入器从控制台读入键盘敲击的信息,并将其转换为 Unicode:
Reader in = new InputStreamReader(System.in);
这个 InputStreamReader 会使用主机系统所使用的默认字符编码方式。应该总是在 InputStreamReader的构造器中选择一种具体的编码方式,例如:
Reader in = new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8);

如何写出文本输出

可以使用 PrintWriter 完成文本输出的任务,这个类拥有以文本格式打印字符串和数字的方法,可以使用下面的方法构造:
PrintWriter out = new PrintWriter("employee.txt", "UTF-8");
为了输出到 PrintWriter,需要使用与使用 System.out 时相同的 print、println 和 printf 方法。
你可以用这些方法来打印数字(int、short、long、float、double)、字符、boolean 值和对象。

如何读入文本输入

最简单的处理任意文本的方式就是使用 Scanner 类,我们可以从任何输入流中构建 Scanner 对象。

1
2
3
4
5
6
7
8
9
10
try (Scanner in = new Scanner(new InputStreamReader(inputStream,
StandardCharsets.UTF_8))) {
String line;
while (in.hasNextLine()) {
// do something with line
line = in.nextLine();
int a = in.nextInt();
double b = in.nextDouble();
}
}

或者,我们也可以将短小的文本文件像下面这样读入到一个字符串中:
String content = new String(Files.readAllBytes(path), charset);
如果想要将这个文件按行读入,可以调用:
List<String> lines = Files.readAllLines(path, charset);
如果文件太大,那么可以将行惰性处理为一个 Stream 对象:

1
2
3
try (Stream<String> lines = Files.lines(path, charset)) {
...
}

在早期的 Java 版本中,处理文本输入的唯一方式就是通过 BufferedReader 类。它的 readLine 方法会产生一行文本,或者在无法获得更多输入时返回 null。典型的输入循环看起来像下面这样:

1
2
3
4
5
6
7
8
InputStream inputStream = Files.newInputStream(Paths.get("employee.txt"));
try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,
StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
// do something with line
}
}

如今,BufferedReader 类又有了一个 lines 方法,可以产生一个 Stream 对象,但是与 Scanner 不同,BufferedReader 没有用于任何读入数字的方法。

字符编码方式

Java 针对字符使用的是 Unicode 标准,每个字符或 “编码点” 都对应一个 21 位的整数。基于 Unicode 标准有很多种编码方式。
编码方式:指将 Unicode 字符对应的 21 位整数转换成 字节 的方式。

最常见的编码方式是 UTF-8, 它会将每个 Unicode 编码点编码为 1 到 4 个字节的序列,也就是说 UTF-8 是一种可变长的编码方式。
UTF-8 的好处是传统的包含了英语中所有用到字符的 ASCII 字符集中每个字符经过 UTF-8 编码后都只占用一个字节。
表2-1 UTF-8 编码方式

字符范围 编码方式
0…7F 0a6a5a4a3a2a1a0
80…7FF 110a10a9a8a7a6 10a5a4a3a2a1a0
800…7FFF 1110a15a14a13a12 10a11a10a9a8a7a6 10a5a4a3a2a1a0
10000…10FFFF 11110a20a19a18 10a17a16a15a14a13a12 10a11a10a9a8a7a6 10a5a4a3a2a1a0

另一种常见的编码方式是 UTF-16,它会将每个 Unicode 编码点编码为 1 个或 2 个 16 位值。这是一种在 Java 字符串中使用的编码方式。实际上,有两种形式的 UTF-16,分别是 高位优先低位优先 。对于 16 位值 0x2122,在高位优先格式中,高位字节会先出现:0x21 后面跟着 0x22;但是在低位优先格式中,则是 0x22 后面跟着 0x21 。为了表示使用的是哪一种格式,文件可以以 “字节顺序标记” 开头,这个标记为 16 位数值 0xFEFF。读入器可以使用这个值来确定字节顺序,然后丢弃它。

表2-2 UTF-16 编码方式

字符范围 编码方式
0…FFFF a15a14a13a12a11a10a9a8 a7a6a5a4a3a2a1a0
10000…10FFFF 110110b19b18 b17b16a15a14a13a12a11a10 110111a9a8 a7a6a5a4a3a2a1a0
其中 b19b18b17b16 = a20a19a18a17a16 - 1

注:某些程序会在 UTF-8 编码的文件开头处添加一个字节顺序标记,虽然 UTF-8 标准中不存在字节顺序的问题,但 Unicode 标准允许添加字节顺序标记。因此需要注意将输入中发现的所有先导的 \uFEFF 字节顺序标记丢弃掉。

除了 UTF 编码方式外,还有一些编码方式,它们各自有覆盖了适用于特定用户人群的字符范围。
例如,ISO 8859-1 是一种单字节的编码,它包含了西欧各种语言中用到的带有重音符号的字符,而 Shift-JIS 是一种用于日文字符的可变长编码。

不存在任何可靠的方式可以自动地探测出字节流中所使用的字符编码方式,因此应该总是明确指定编码方式。例如,在编写网页时,应该检查 Content-Type 头信息。

  • 平台使用的编码方式可以使用静态方法 Charset.defaultCharset() 返回。
  • Charset.availableCharsets() 方法会返回所有可用的 Charset 实例

StandardCharsets 类具有类型为 Charset 的静态变量,用于表示每种 Java 虚拟机都必须支持的字符编码方式:

  • StandardCharsets.UTF_8
  • StandardCharsets.UTF_16
  • StandardCharsets.UTF_16BE
  • StandardCharsets.UTF_16LE
  • StandardCharsets.ISO_8859_1
  • StandardCharsets.US_ASCII

为了获得另一种编码方式的 Charset,可以使用静态的 forName 方法:Charset shiftJIS = Charset.forName("Shift-JIS");
在读入或写出文本时,应该使用 Charset 对象。例如我们可以像下面这样将一个字节数组转换为字符串:
String str = new String(bytes, StandardCharsets.UTF_8);

注意: 在不指定任何编码方式时,有些方法(例如 String(byte[]) 构造器)会使用默认的平台编码方式(Charset.defaultCharset()),而其他方法(例如 Files.readAllLines)会使用 UTF-8 。

读写二进制数据

DataInput 和 DataOutput 接口

DataOutput 接口定义了下面用于以二进制格式写数组、字符、boolean 值和字符串的方法:

  • writeChars(String s): 写出字符串中的所有字符
  • writeByte(int b)
  • writeInt(int i): 将一个整数写出为 4 字节的二进制数量值,不管整数本身的位数
  • writeShort(int s)
  • writeLong(long l)
  • writeFloat(float f)
  • writeDouble(double d): 将一个 double 值写出为 8 字节的二进制数量值。
  • writeChar(int c)
  • writeBoolean(boolean b)
  • writeUTF: 使用修订版的 8 位 Unicode 转换格式写出字符串。这种方式和直接使用标准的 UTF-8 编码方式不同,其中,Unicode 码元序列首先用 UTF-16 表示,其结果之后使用 UTF-8 规则进行编码。(修订后的方式对于编码大于 0xFFFF 的字符的处理有所不同,这是为了向后兼容在 Unicode 还没有超过 16 位时构建的虚拟机)因为没有其他方法会使用 UTF-8 的这种修订,所以你应该只在写出用于 Java 虚拟机的字符串时才使用 writeUTF 方法,例如,当你需要编写一个生成字节码的程序时。对于其他场合,都应该使用 writeChars 方法。

为了读回数据,可以使用 DataInput 接口中定义的下列方法:

  • readInt
  • readShort
  • readLong
  • readFloat
  • readDouble
  • readChar
  • readBoolean
  • readByte
  • readUTF
  • readFully(byte[] b): 将字节读入到数组 b 中,其间阻塞直至所有字节都读入
  • readFully(byte[] b, int off, int len): 将字节读入数组 b,其间阻塞直至所有字节都读入,off 数据其实位置的偏移量,len 读入字节的最大数量
  • skipBytes(int n) 跳过 n 个字节,其间阻塞直至所有字节都被跳过

DataInputStream 类实现了 DataInput 接口,为了从文件中读入二进制数据,可以将 DataInputStream 和某个字节源组合,例如 FileInputStream:
DataInputStream in = new DataInputStream(new FileInputStream("employee.dat"));
与此类似,要想写出二进制数据,你可以使用实现了 DataOutput 接口的 DataOutputStream 类:
DataOutputStream out = new DataOutputStream(new FileOutputStream("employee.dat"));

Java 中所有值都是按照高位在前的模式写出的。

随机访问文件

RandomAccessFile 类可以实现在文件中的任何位置查找或写入数据,磁盘文件都是随机访问的,但是网络套接字通信的输入/输出流却不是。你可以打开一个随机访问文件,只用于读入或者同时用于读写,你可以通过使用字符串 “r”(用于读入访问) 或 “rw”(用于读入/写出访问)作为构造器的第二个参数来指定这个选项。

1
2
RandomAccessFile in = new RandomAccessFile("employee.dat", "r");
RandomAccessFile inOut = new RandomAccessFile("employee.dat", "rw");

当你将已有文件作为 RandomAccessFile 打开时,这个文件并不会被删除。
RandomAccessFile 有一个表示下一个将被访问(读入或者写出)的字节所处位置的文件指针,seek 方法可以用来将这个文件指针设置到文件中的任意字节位置,seek 的参数时一个 long 类型的整数,它的值位于 0 到文件按照字节来度量的长度之间。
RandomAccessFile 其他的一些方法:

  • long getFilePointer 方法将返回文件指针的当前位置。
  • long length() 方法返回文件按照字节来度量的长度。
  • RandomAccessFile 类同时实现了类 DataInput 和 DataOutput 接口。

ZIP 文档

对象输入/输出流与序列化

操作文件

内存映射文件

正则表达式

NIO

待搜索

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022 qusong
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信