java编程第六讲:愉快地学Java语言第十六章
java编程第六讲:愉快地学Java语言第十六章public abstract class OutputStream implements Closeable Flushable { ... //将b写入输出流,一般是将b的8个低阶位写入输出流, //忽略b的24个高 阶位。 public abstract void write(int b) throws IOException; //将b[off] b[off l] … b[off len-l]写入输出流中 public void write(byte b[] int off int len) throws IOException {...} ... } 2 二进制文件读写2.1 FileInputStream和FileOutputStreamFileInputStream从文件中读取,FileOutputStream将数据写入文件。这两个类是用于读写二进制数据
导读本文适合Java入门,不太适合Java中高级软件工程师。本文以《Java语言程序设计基础》第10版为蓝本,采用不断提出问题,然后解答问题的方式来讲述。本篇文章只是这个系列中的一篇,如果你喜欢这种讲解方式,或者觉得从中能学到知识,可以关注我,以便查阅本系列其他文章。
让我们开始愉快地学习Java语言吧!
1输入/输出流输入流:能够从目标资源中读出一个字节序列的对象
输出流:能够向目标资源中写入一个字节序列的对象
InputStream、OutputStream这两个抽象类构成输入输出流的基类。
下面是InputStream的定义,列出了一些方法。InputStream实现了Closeable接口,这表明可以使用try语句来关闭资源。
public abstract class InputStream implements Closeable{ ... //从输入流中读取下一个字节数据。返回值范围为0到255。如果已经达到流 //的最后,则返回值-1。 public abstract int read() throws IOException; //从输入流中读取b.length个字节到数组b中,并且返回实际读取的宇节数。 //如果已经达到流的最后,则返回值-1。 public int read(byte b[]) throws IOException{...} //从输入流中读取len个字节,将它们依次存储到 //b[off] b[off 1] … b[off len-1] 中,返回实际读取的字节数, //也就是说,实际读取的字节数可能小于len public int read(byte b[] int off int len)throws IOException{...} //跳过并丢弃输入流中前n个字节的数据 public long skip(long n) throws IOException {...} ... }
下面是OutputStream的定义,这里列出一些方法。
public abstract class OutputStream implements Closeable Flushable { ... //将b写入输出流,一般是将b的8个低阶位写入输出流, //忽略b的24个高 阶位。 public abstract void write(int b) throws IOException; //将b[off] b[off l] … b[off len-l]写入输出流中 public void write(byte b[] int off int len) throws IOException {...} ... } 2 二进制文件读写2.1 FileInputStream和FileOutputStream
FileInputStream从文件中读取,FileOutputStream将数据写入文件。这两个类是用于读写二进制数据的。
其实文本文件和二进制文件并没有本质的区别,所有文件在计算机上都是以二进制存储的。只不过对文本文件的读写涉及到字符编码和解码的过程,而二进制文件不需要这个过程,当然对二进制文件加密与解密是另外一种情况。
让我们看一下使用FileInputStream读取一个文本文件会得到什么结果,文件名为test文本文件的内容为:文件读写操作。
下面举一个例子,读取图片,然后写入一个新文件中,和拷贝是一个效果。
在相应的文件夹下确实可以看到操作结果。
在讲文本文件读写的时候,没有给出追加写操作,现在我们利用Fi1eOutputStream来实现文本文件追加写。
为什么要使用过滤器类呢?
FileInputStream和FileOutputStream只能读写二进制数据,他们无法读取基本数据类型和字符串,这样就需要使用一个类将字节流包装一下,FileInputStream和FileOutputStream正式提供这种功能的包装类。
那么如何使用这两个包装类呢?
先看一下这两个类的定义:
public class FilterInputStream extends InputStream { protected volatile InputStream in; protected FilterInputStream(InputStream in) { this.in = in;} ... } public class FilterOutputStream extends OutputStream{ protected OutputStream out; public FilterOutputStream(OutputStream out) { this.out = out; } ... }
上面仅列出了构造方法,构造方法有一个参数,过滤器就是对这个参数类型的包装。通过查看源码发现,这两个过滤器没有实现操作基本类型的方法。如果要操作基本类型要使用DatalnputStream和DataOutputStream来操作基本数据类型。
2.3 DataInputStream和DataOutputStream我们来看DataInputStream的定义,并且只分析它的实现,DataOutputStream和DataInputStream实现基本类似。
public class DataInputStream extends FilterInputStream implements DataInput { public DataInputStream(InputStream in) { super(in); } public final boolean readBoolean() throws IOException { int ch = in.read(); if (ch < 0) throw new EOFException(); return (ch != 0); } public final short readShort() throws IOException { int ch1 = in.read(); int ch2 = in.read(); if ((ch1 | ch2) < 0) throw new EOFException(); return (short)((ch1 << 8) (ch2 << 0)); } ... }
DataInputStream的构造方法有一个形参,它就是输入流。DataInputStream扩展了FilterInputStream,并在其构造方法中调用父类的构造方法,将输入流保存到私有变量in。
DataInputStream还实现了DataInput接口,这个接口中定义了读取基本数据类型的方法。
以readBoolean为例,调用in的read方法读取一个比特数据,最后返回的是(ch != 0),也就是说不为零,那么一定返回true。
readShort方法的实现过程与readBoolean类似,也是调用in的read方法读取一个比特数据,最终将其转换为short类型。
DataInputStream提供的方法用法比较简单,如果有不清楚的地方,可以随时查看API文档或者源码。
下面对DataOutputStream的几个方法说明一下。
DataOutputStream的几个方法都是将参数编码后再执行操作的。需要注意的是writeBytes(String s)方法将字符串s中每个字符进行编码,丢弃高字节,然后将低字节写到输出流。这里采用的编码为Unicode码。
有一个问题就要特别注意了,writeBytes将高字节丢弃,所以此方法无法用于非ASCII编码的字符;ASCII使用低字节(低八位)编码,所以使用writeBytes处理没有问题。
上述无法使用writeBytes操作的数据,可以使用writeChars(String s),因为这个方法不会将s的高字节丢弃。
writeUTF(String s)这个方法将字符串转换为改进版UTF-8编码,然后写入文件。使用改进版UTF-8编码,那么系统就可以同时处理Unicode编码和ASCII编码的数据了。改进版UTF-8编码方案根据实际情况将字符编码为1字节、2字节、3字节。如果字符的编码值小于或等于0x7F就将该字符编码为一个宇节 如果字符的编码值大于 0X7F而小于或等于0X7FF就将该字符编码为两个字节 如果该字符的编码值大于0X7FF就将该字符编码为三个字节。
如何判断是用几个字节编码的呢?
如果第一位是0,那么就是一个字节;如果头几位是110,那么就是2字节;如果头几位是1110,那么就是3字节。
标准UTF-8编码和改进版UTF-8编码有啥不同呢?
标准UTF-8也是变长编码,但它使用1到4个字节。
改进版UTF-8中,null字符编码成2个字节1100000010000000(十六进制为C080)而不是标准的1个字节00000000(十六进制为00),这样做可以保证编码后的字符串不会嵌入null字符,如果在类C语言中处理字符串,文本不会在第一个null字符时截断(C字符串以'\0'结尾)(见https://en.wikipedia.org/wiki/UTF-8和https://baike.baidu.com/item/UTF-8/481798?fr=aladdin)。
下面是一个例子,我们看到确实将字符串写入件,但是还抛出了异常,这是为什么呢?
我们在while循环中没有考虑到达流末尾的情形。如果已达到文件末尾还继续读取数据,那么就会抛出异常EOFException。如何验证已到达流末尾呢?遗憾的是没有一个方便的方法检测是否到达流的末尾,只能通过抛出异常来判断是否达到流的末尾。
2.4 BufferedlnputStream 和 BufferedOutputStream这两个类也是读写文件用的,前面已经介绍那么多读写文件的类了,为啥还要介绍着两个类呢?
假如有两种选择,一种是多次操作写入文件,另一种是先将数据缓存到内存中,等到积累到一定量时,在将当前内存中的数据写入文件。你会选择哪一种方式呢?
我想应该选择第二中,因为第二种方式减少了操作文件的次数,因此性能更高。当然,你可能考虑到将数据先存到内存中会消耗大量内存资源,不过,不用担心,如果控制得好不会出现内存不够用的情况。
默认的缓冲区为8192字节。也可以通过构造方法设定缓冲区大小。
public BufferedInputStream(InputStream in int size)
但令人不解的是,BufferedOutputStream的构造方法也将缓冲区默认为8192字节,不过没有定义为常量,这不太合乎一般的规约。
public BufferedOutputStream(OutputStream out) { this(out 8192); }
怎么使用这两个类呢?
BufferedInputStream父类是FilterInputStream,BufferedOutputStream父类是FilterOutputStream,所以和这两个类的使用方法类似。
3 压缩文件读写使用ZipOutputStream和ZipInputStream可以实现文件的压缩与解压缩。
下面给出一个例子,将一个字符串写入压缩文件,然后在将其解压到内存中,最后打印出来。
可以实现基本数据类型与字符串的输人和输出,还可以用于读写可序列化的对象。
下面是使用这两个类的一个例子。
我发现,上面这个例子无法实现追加写操作,FileOutputStream有相应的构造方法:public FileOutputStream(String name boolean append),将append设置为true就可以实现追加写,但是这里如果这么设置会抛异常。
在说明对象序列化之前,先看看Serializable接口。
可写入流中的对象称之为可序列化对象,要想使一个对象可序列化,那么它必须实现Serializable接口(它是一个标记接口)。
看下面的例子,这个例子中定义了一个类,它并没有实现Serializable接口,按照我们的预想,确实抛出不可序列化异常。
如果一个对象包含不可序列化的数据域,那么该如何处理这种情形呢?
在数据域上使用transient关键字 这样Java虚拟机将对象写入流时就会忽略这些数据域。
重复序列化一个对象,那么会向流中写入多个对象吗?
不会重复写入,第一次写入一个对象时,创建一个序列号,将对象的所有内容和序列号一起写入对象流。重复序列化一个对象时,只存储这个列号。
下面改造上个例子中的SerialObj,使其可序列化,然后运行程序,查看结果。
不过有一个特殊的类型,虽然它没有实现Serializable接口,但是它也是可以序列化的,它就是数组。
RandomAccessFile允许从文件的任何位置读写。
为什么要使用RandomAccessFile?
假设我要对文件的一部分修改,而我不想重写整个文件,但是之前介绍的那些操作文件的类都没法完成这项任务,恰好RandomAccessFile可以完成这项任务。
我们称前面介绍的那些流类为顺序流,它们只提供读写功能。
我们看一下构造方法:
public RandomAccessFile(String name String mode)
构造方法有两个参数,一个是文件名,一个是操作模式。
都有哪些操作模式呢?
“r”,“rw”,“rws”,“rwd”。r表示只读模式,rw表示可读可写模式,如果文件不存在就创建它,rws表示可读可写模式,并且对文件内容或元数据的每次更新都同步写入基础存储设备,rwd表示可读可写模式,并且对文件内容的每次更新都同步写入基础存储设备。
看下面一个例子,反复运行发现默认不是追加写而是覆盖写。
设置文件指针位置,读取第二个长整型,因为seek的参数是以字节为单位的,长整型为64位,即8字节,所以设置ra.seek(8)。