二进制 IO (Java)

二进制 I/O

Java 提供了许多类用于实现文本 I/O 和二进制 I/O 。

文件可以分类为文本文件和二进制文件。能够使用文本编辑器,比如 Windows 下的记事本或者 UNIX 下的 vi 编辑器,进行处理(读取、创建或者修改)的文件称为文本文件。其他所有的文件称为二进制文件。它们是为特定程序读取而设计的,不能使用文本编辑器来读取二进制文件。例如,Java 源程序存储在文本文件中,可以使用文本编辑器取,而 Java 字节码是二进制文件,由 Java 虚拟机读取。

文本文件是由字符序列构成的,而二进制文件是由位(bit) 序列构成的。二进制文件的优势在于它的处理效率比文本文件高。

Java 提供了许多实现文件输人/输出的类。这些类可以分为文本 I/O 类(text I/O class) 和二进制 I/O 类、(binary I/O class)。

在 Java 中处理文本 I/O

使用 Scanner 类读取文本数据,使用 PrintWriter 类写文本数据。

File 对象封装了文件或路径属性,但是不包含从/向文件读/写数据的方法。为了进行 I/O 操作,需要使用正确的 Java I/O 类创建对象。这些对象包含从/向文件中读/写数据的方法。

Java 有许多用于各种目的的 I/O 类。通常,可以将它们分为输人类和输出类。榆入类包含读数据的方法,而输出类包含写数据的方法。PrintWriter 个输出类的例子,而 Scanner 是一个输入类的例子。输人对象从文件中读取数据流,输出对象将数据流写人文件。输入对象也称作榆入流(input stream)。同样,输出对象也称作输出流(output stream )。

文本 I/O 与二进制 I/O

二进制 I/O 不涉及编码和解码,因此比文本 I/O 更加高效。

计算机不区分二进制文件与文本文件,即所有的文件都是以二进制形式来存储的。从本质上说,所有的文件都是二进制文件。

文本 I/O 建立在二进制 I/O 的基础之上,它能提供一层抽象,用于字符层次的编码和解码。对于文本 I/O 而言,编码和解码是自动进行的。在写人一个字符时,Java 虚拟机会将统一码转化为文件指定的编码,而在读取字符时,将文件指定的编码转化为统一码。注意,Windows 系统中文本文件的默认编码方案是 ASCII 码。

二进制 I/O 不需要转化。如果使用二进制 I/O 向文件写入一个数值,就是将内存中的那个值复制到文件中。使用二进制 I/O 读取一个字节时,就会从输入流中读取一个宇节的数值。

一般来说,对于文本编辑器或文本输出程序创建的文件,应该使用文本输入来读取,对于 Java 二进制输出程序创建的文件,应该使用二进制输人来读取。

二进制 I/O 不需要编码和解码,比文本 I/O 效率高。二进制文件与主机的编码方案无关,它是可移植的。在任何机器上的 Java 程序可以读取 Java 程序所创建的二进制文件。Java 的类文件也存储为二进制文件。Java 类文件可以在任何具有 Java 虚拟机的机器上运行。

二进制 I/O 类

抽象类 InputStream 是读取二进制数据的根类,抽象类 OutputStream 是写入二进制数据的根类。

Java I/O 类的设计应用了继承,它们的公共操作是由父类生成的,而子类提供特定的操作。InputStream 类是二进
制输人类的根类,而 OutputStream 类是二进制输出类的根类。

注意: 二进制 I/O 类中的所有方法都声明为抛出 java.io.IOException 或 java.io.I0Exception 的子类。

FileInputStream 和 FileOutputStream

FileInputStream 类和 Fi1eOutputStream 类用于从/向文件读取/写人字节。它们的所有方法都是从 InputStream 类和 OutputStream 类继承的。FilelnputStream 类和 FileOutputStream 类没有引入新的方法。为了构造一个 FilelnputStream 对象,使用下面的构造方法:



如果试图为一个不存在的文件创建 FilelnputStream 对象,将会发生 java.io.FileNotFoundExcept!on 异常。

要构造一个 FileOutputStream 对象,使用下面的构造方法:


如果这个文件不存在,就会创建一个新文件。如果这个文件已经存在,前两个构造方法将会删除文件的当前内容。为了既保留文件现有的内容又可以给文件追加新数据,将最后两个构造方法中的参数 append 设置为 true 。

几乎所有的 I/O 类中的方法都会抛出异常 java.io.IOException 。因此,必须在方法中声明会抛出java.io.IOException 异常,或者将代码放到 try-catch 块中处理。

使用 try-with-resources 来声明和创建输人输出流,从而在使用后可以自动关闭。java.io.InputStream 和 java.io.OutputStream 实现了 AutoClosable 接口。AutoClosable 接口定义了close() 方法,用于关闭资源。任何 AutoClosable 类型的对象可以用于 try-with-resources 语法中,实现自动关闭。

提示: 当流不再需要使用时,记得使用 closeO 方法将其关闭,或者使用 try-with-resource 语句自动关闭。不关闭流可能会在输出文件中造成数据受损,或导致其他的程序设计错误。

注意: FilelnputStream 类的实例可以作为参数去构造一个 Scanner 对象, 而 FileOutputStream 类的实例可以作为参数构造一个 PrinterWrVter 对象。可以创建一个 PrinterWriter 对象来向文件中追加文本。

FilterInputStream 和 FilterOutputStream
过滤器数据流 (filter stream) 是为某种目的过滤字节的数据流。基本字节输人流提供的读取方法 read 只能用来读取字节。如果要读取整数值、双精度值或字符串,那就需要一个过滤器类来包装字节输入流。使用过滤器类就可以读取整数值、双精度值和字符串,而不是字节或字符。FilterlnputStream 类和 FilterOutputStream 类是过滤数据的基类。需要处理基本数值类型时,就使用 DatalnputStream 类和 DataOutputStream 类来过滤字节。

DataInputStream 和 DataOutputStream
DataInputStream 从数据流读取字节,并且将它们转换为合适的基本类型值或字符串。DataOutputStream 将基本类型的值或字符串转换为字节,并且将字节输出到数据流。

DataInputStream 类扩展 FilterInputStream 类,并实现 Datalnput 接口。DataOutputStream 类扩展 FilterOutputStream 类,并实现 DataOutput 接口。

DatalnputStream 实现了定义在 Datalnput 接口中的方法来读取基本数据类型值和字符串。DataOutputStream 实现了定义在 DataOutput 接口中的方法来写入基本数据类型值和字符串。基本类型的值不需要做任何转化就可以从内存复制到输出数据流。

二进制 I/O 中的宇符与宇符串
一个统一码由两个字节构成。writerChar(char c) 方法将字符 c 的统一码写入输出流。writerChars(String s) 方法将字符串 s 中所有宇符的统一码写到输出流中。writeBytes(String s) 方法将字符串 s 中每个字符统一码的低字节写到输出流,统一码的高字节被丢弃。

writeBytes 方法适用于由 ASCII 码字符构成的字符串,因为 ASCII 码仅存储统一码的低字节。如果一个字符串包含非 ASCII 码的字符,必须使用 writeChars 方法实现写入这个字符串。

writeUTF(String s) 方法将两个字节的长度信息写入输出流,后面紧跟的是字符串 s 中每个字符的改进版 UTF-8 的形式。WriteUTF(Str1ng s) 方法将字符串转化成 UTF-8 格式的一串字节,然后将它们写人一个输出流。readUTF() 方法读取一个使用 writeUTF 方法写人的字符串。

UTF-8 是一种编码方案,它允许系统可以同时操作统一码及 ASCII 码。大多数操作系统使用 ASCII 码,Java 使用统一码。ASCII 码字符集是统一码字符集的子集。

由于许多应用程序只需要 ASCII 码字符集,所以将 8 位的 ASCII 码表示为 16 位的统一码是很浪费的。UTF-8 的修改版方案分别使用 1 字节、2字节或 3 字节来存储字符。

  • 如果字符的编码值小于或等于 0x7F 就将该字符编码为一个宇节,
  • 如果字符的编码值大于 0X7F 而小于或等于 0X7FF 就将该字符编码为两个字节,
  • 如果该字符的编码值大于 0X7FF 就将该字符编码为三个字节。

UTF-8 字符起始的几位表明这个字符是存储在一个宇节、两个字节还是三个字节中。

  • 如果首位是 0, 那它就是一个字节的字符。
  • 如果前三位是 110, 那它就是两字节序列的第一个宇节。
  • 如果前四位是 1110, 那它就是三字节序列的第一个字节。

UTF-8 字符之前的两个字节用来存储表明字符串中的字符个数的信息。

UTF-8 格式具有存储每个 ASCII 码就节省一个字节的优势,因为一个统一码字符的存储需要两个字节,而在 UTF-8 格式中 ASCII 字符仅占一个字节。如果一个长字符串的大多数字符都是普通的 ASCII 字符,采用 UTF-8 格式存储更加高效。

创建 DatalnputStream 类和 DataOutputStream 类
使用下面的构造方法来创建 DatalnputStream 类和 DataOutputStream 类

public DataInputStream(InputStream instream)
public DataOutputStream(OutputStream outstream)

DatalnputStream 类和 DataOutputStream 类以同机器平台无关的方式读写 Java 基本类型值和字符串,因此,如果在一台机器上写好一个数据文件,可以在另一台具有不同操作系统或文件结构的机器上读取该文件。应用程序可以利用数据输出流写入数据,之后某个程序可以利用数据输人流读取这个数据。

DatalnputStream 将一个输人流的数据过滤成合适的基本类型值或者字符串。DataOutputStream 将基本类型值或者字符串转换成字节并且输出字节到输出流中。

警告: 应该按存储的顺序和格式读取文件中的數据。

检测文件的末尾
如果到达 InputStream 的末尾之后还继续从中读取数据,就会发生 EOFException 异常。这个异常可以用来检査是否已经到达文件末尾。

BufferedlnputStream 和 BufferedOutputStream
BufferedlnputStream 类和 BufferedOutputStream 类可以通过减少磁盘读写次数来提髙输人和输出的速度。使用 Bufferdl叩utStream 时,磁盘上的整块数据一次性地读入到内存中的缓冲区中。然后从缓冲区中将个别的数据传递到程序中。使用 BufferedOutputStream, 个别的数据首先写人到内存中的缓冲区中。当缓冲区已满时,缓冲
区中的所有数据一次性写入到磁盘中。

BufferedlnputStream 类和 BufferedOutputStream 类没有包含新的方法。BufferedInputStream 类和 BufferedOutputStream 中的所有方法都是从 InputStream 类和 OutputStream 类继承而来的。BufferedlnputStream 类和 BufferedOutputStream 类在后台管理了一个缓冲区,根据要求自动从磁盘中读取数据和写入数据。

注意: 如果没有指定缓冲区大小,默认的大小是 S12 个字节。

提示:应该总是使用缓冲区 I/O 来加速输入和输出。对于小文件,我们可能注意不到性能的提升。但是,对于超过 100MB 的大文件,我们将会看到使用缓冲的 I/O 带来的实质性的性能提升。

对象 I/O

ObjectlnputStream 类和 ObjectOutputStreara 类可以用于读 / 写可序列化的对象。

DatalnputStream 类和 DataOutputStream 类可以实现基本数据类型与字符串的输人和输出。而 ObjectlnputStream 类和 ObjectOutputStream 类除了可以实现基本数据类型与字符串的输人和输出之外,还可以实现对象的输人和输出。由于 ObjectlnputStream 类和 ObjectOutputStream 类包含 DatalnputStream 类和 DataOutputStream 类的所有功能,所以,完全可以用 ObjectlnputStream 类和 ObjectOutputStream 类代替 DatalnputStream 类和 DataOutputStream 类。

ObjectInputStream 扩展 InputStream 类,并实现接口 ObjectInput 和 ObjectStreamConstants。ObjectInput 是 Datalnput 的子接口。ObjectStreamConstants 包含支持 ObjectlnputStream 类和 ObjectOutputStream 类所用的常量。ObjectOutputStream 扩展 OutputStream 类,并实现接口 ObjectOutput 与 ObjectStreamConstants 。ObjectOutput 是 DataOutput的子接口。

readObject() 方法可能会拋出异常 ]ava.lang.ClassNotFoundException。这是因为 Java 虚拟机恢复一个对象时,如果没有加载该对象所在的类,就应该先加载这个类。因为 ClassNotFoundException 异常是一个必检异常,必须在方法中声明抛出它。

Serializable 接口

并不是每一个对象都可以写到输出流。可以写人输出流中的对象称为可序列化的 (serializable)。可序列化的对象是 java.io.Serializable 接口的实例,可序列化对象的类必须实现 Serializable 接口。

Serializable 接口是一种标记接口。因为它没有方法,所以,不需要在类中为实现 Serializable 接口增加额外的代码。实现这个接口可以启动 Java 的序列化机制,自动完成存储对象和数组的过程。Java 提供一个内在机制自动完成写对象的过程。这个过程称为对象序列化 ( object serialization), 它是在 ObjectOutputStream 中实现的。与此相反,读取对象的过程称作对象反序列化(object deserialization), 它是在 ObjectlnputStream 类中实现的。

许多 Java API 中的类都实现了 Serializable 接口。所有针对基本类型值的包装
类,java.math 、Biglnteger 、java.math.BigDecimal 、java.lang.String 、java.lang.StringBuilder 、java.lang.StrlngBuffer 、java.util.Date 以及 java.util.ArrayList 都实现了 java.io.Serializable 接口。试图存储一个不支持 Serializable 接口的对象会引起一个 NotSerializableException 异常。

当存储一个可序列化对象时,会对该对象的类进行编码。编码包括类名、类的签名、对象实例变量的值以及该对象引用的任何其他对象的闭包,但是不存储对象静态变量的值。

注意: 如果一个对象是 Serializable 的实例,但它包含了非序列化的实例数据域,那么不可以序列化这个对象。为了使该对象是可序列化的,需要给这些数据域加上关键字 transient, 告诉 Java 虚拟机将对象写入对象流时忽略这些数据域。

注意: 如果一个对象不止一次写入对象流,不会存储对象的多份副本。第一次写入一个对象时,就会为它创建一个序列号。Java 虚拟机将对象的所有内容和序列号一起写入对象流。以后每次存储时,如果再写入相同的对象,就只存储序列号。读出这些对象时,它们的引用相同,因为在内存中实际上存储的只是一个对象。

序列化数组

如果数组中的所有元素都是可序列化的,这个数组就是可序列化的。一个完整的数组可以用 writeObject方法存入文件,随后用 readObject 方法恢复。

随机访问文件

Java 提供了 RandomAccessFile 类,允许从文件的任何位置进行數据的读写。

使用的 I/O 流一般都是只读的(read.only) 或只写的(write.only)。这些流称为顺序(sequential)流。使用顺序流打开的文件称为顺序访问文件。顺序访问文件的内容不能更新。然而,经常需要修改文件。Java 提供了 RandomAccessFile 类,允许在文件的任意位置上进行读写。使用 RandomAccessFile 类打开的文件称为随机访问文件。

RandomAccessFile 类实现了Datalnput 和 DataOutput 接口。Datalnput 接口定义了读取基本数据类型和字符串的方法。DataOutput 接口定义了输出基本数据类型和字符串的方法。

当创建一个 RandomAccessFile 时,可以指定两种模式(“r” 或 “rw”) 之一。模式 “r” 表明这个数据流是只读的,模式 “rw” 表明这个数据流既允许读也允许写。

提示: 如果不想改动文件,就将文件以 “r” 模式打开。这样做可以防止不经意中改动文件。

随机访问文件是由字节序列组成的。一个称为文件指针 (file pointer) 的特殊标记定位这些字节中的某个字节的位置。文件的读写操作就是在文件指针所指的位置上进行的。打开文件时,文件指针置于文件的起始位置。在文件中进行读写数据后,文件指针就会向前移到下一个数据项。

设 raf 是 RandomAccessFile 的一个对象,可以调用 raf.seek(position) 方法将文件指针移到指定的位置。raf.Seek(0) 方法将文件指针移到文件的起始位置,而 raf.Seek(raf.length()) 方法则将文件指针移到文件的末尾。raf.length() 方法返回在给定时刻文件中的宇节数。如果向文件中追加新数据,raf.length() 就会增加。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值