Java的IO系统(2) - 字节流类

发表于2019-12-25,长度7650, 1166个单词, 21分钟读完
Flag Counter

Java的IO系统针对输入和输出分别有两个基类:字节输入流InputStream、字符输入流Reader,字节输出流OutputStrea、字符输出流Writer。在操作字符或字符串时应该使用字符流,在操作字节或其他二进制格式时,应使用字节流。这里先介绍字节流的常用实现类。

InputStream类

InputStream是字节输入的抽象父类,实现了Closeable接口,可能会抛出IOException异常。主要方法如下(按字母序排列):

IO异常不是运行时异常,所以要提前准备好处理方案

方法 描述
int available( ) 返回当前可读取的输入字节数
void close( ) 关闭输入源。如果试图继续进行读取,会产生IOException异常
void mark(int numBytes). 在输入流的当前位置放置标记,该标记在读入numBytes个字节之前一直都有效
boolean markSupported( ) 如果调用流支持mark或reset方法,就返回true
int read( ) 返回代表下一个可用字节的整数。当到达文件末尾时,返回-1
int read(byte buffer[ ]) 尝试读取buffer.length个字节到buffer中,并返回实际成功读取的字节数。 如果到达文件末尾,就返回-1
int read(byte buffer[ ], int offset, int numBytes) 尝试读取numBytes个字节到buffer中,从buffer[offset]开始保存读取的字节。该方法返回成功读取的字节数;如果到达文件末尾,就返回-1
void reset( ) 将输入指针重置为前面设置的标记
long skip(long numBytes) 忽略(即跳过)numBytes个字节的输入,返回实际忽略的字节数

其中mark和reset方法不一定需要具体实现,它们是同步方法,需要用到的场景实现即可,并将markSupported置为true。

OutputStream

字节输出流的抽象父类实现了Closeable接口和Flushable接口。主要有以下方法,基本都无返回值:

方法 描述
void close( ) 关闭输出流。如果试图继续向流中写入内容,将产生IOException异常
void flush( ) 结束输出状态,从而清空所有缓冲区,即冲刷输出缓冲区
void write(int b) 向输出流中写入单个字节。注意参数是int类型,从而允许使用表达式调用write方法,而不用将表达式强制转换回byte类型
void write(byte buffer[]) 向输出流中写入一个完整的字节数组
void write(byte buffer[],int offset,int numBytes) 将buffer数组中从buffer[offset]开始的numBytes个字节写入输出流中


两个基类中方法都不多,尽量仔细看一遍有了印象都。下面说一下他们的实现类。

FileInputStream

FileInputStream类专门用于从文件读取字节,它的构造函数要么接受一个字符串的文件路径,要么接受一个File格式的文件对象,文件找不到都会抛出FileNotFoundException异常。建议使用第二个构造方法,可以在创建流之前对文件进行校验等工作。文件流没有重写mark方法,调用reset会报异常。

下面是简单的演示代码(这个类也不复杂):

int size;
try (FileInputStream f = new FileInputStream("/Users/sheldon/IdeaProjects/QLExpress/README.md")) {
    System.out.println("文件总字节数" + (size = f.available()));
    int n = size / 400;
    System.out.println("分400份,头" + n + "个字节:");
    for (int i = 0; i < n; i++) {
        System.out.print((char) f.read());
    }
    System.out.println("\n剩余字节数: " + f.available());
    System.out.println("接下来" + n + "个字符:");
    byte b[] = new byte[n];
    if (f.read(b) != n) {
        System.err.println("不足" + n + "个字符。");
    }
    System.out.println(new String(b, 0, n));
    System.out.println("\n剩余字节:" + (size = f.available()));
    System.out.println("跳过总的一半");
    f.skip(size / 2);
    System.out.println("剩余字节 " + f.available());
} catch (IOException e) {
    e.printStackTrace();
} 

输出如下:

文件总字节数21885
分400份,头 54个字节:
# QLExpress基本语法

[![Join the chat at https://g
剩余字节数: 21831
接下来54个字符:
itter.im/QLExpress/Lobby](https://badges.gitter.im/QLE

剩余字节:21777
跳过总的一半
剩余字节 10889

注意文件里的中文是乱码了,原因很简单,因为一个汉字至少要占用两个字节,一个一个的转码肯定不认识了。后面我们再说中文的处理。

FileOutputStream

这个类用于将字节流写入文件中。有两种构造方法,一种是指定文件的位置(文件可以不存在),第二种会多指定一个参数标明是追加还是覆盖。如果文件打不开或者是一个目录或其他原因没法创建输出流目标,就会报FileNotFoundException异常。

下面是一个例子:我们将一段文本写入文件1,将文本的最后10个字节写入文件2

try (FileOutputStream fos1 = new FileOutputStream("t1.txt");
    FileOutputStream fos2 = new FileOutputStream("t2.txt");) {
    String s = "A distributed transaction solution with high performance and ease of use for microservices architecture.\n" +
            "Distributed Transaction Problem in Microservices\n" +
            "Let's imagine a traditional monolithic application. Its business is built up with 3 modules. They use a single local data source.\n" +
            "Naturally, data consistency will be guaranteed by the local transaction.";
    byte[] bytes = s.getBytes();
    int l = bytes.length;
    fos1.write(bytes);
    fos2.write(bytes, l - 10, 10);
}

如果截取的过程不幸将汉字截断也会乱码,可以自己试一下(比如在字符串末尾增加几个汉字)

ByteArrayInputStream

该类是将字节数组做为流输入,一般来源是字符串。构造函数接收字符数组,也可以额外接受截取的位置和长度。该类重写了mark和reset方法,可以通过例子看一下:

try (ByteArrayInputStream is = new ByteArrayInputStream("abc".getBytes())) {
    boolean again = false; // 记录是否第二次
    while (true) {
        int read = is.read();
        if (read == -1) {
            if (!again) {
                is.reset();
                again = !again;
                read = is.read();
            } else
                break;
        }
        System.out.print(read);
        System.out.print((char) read);

        if (read == 98) {
            is.mark(0);// 传入的参数无效 可以随便写
        }
    }
}

输出:97a98b99c99c

ByteArrayOutputStream

ByteArrayOutputStream有两个变量:buf和count。buf是一个字节数组,默认32,可以通过构造函数变更。每次写入的时候都会写入到该数组,如果数组不够大,就会扩容。count变量记录的是数组已经使用了多大,每次写入都从count的位置写入。同ByteArrayInputStream一样,ByteArrayOutputStream也不需要close(调用了也没事)。

这个类比较简单,就不演示了。一般用法是通过writeTo方法写入到其他输出流(比如FileOutputStream生成文件)。

过滤流FilterInputStream、FilterOutputStream

这两个也算是基类,但没标记为抽象,所以默认行为和传入的流一致。一般都使用他们的子类,他们子类不少,都各有应用场景。

为什么不直接继承抽象基类呢?这是典型的装饰者模式,把需要扩展基类行为的类归在装饰器类下面。

缓冲流 BufferedInputStream、BufferedOutputStream

这两个缓冲流是上面过滤流的子类。缓存IO是常见的性能优化手段,这两个类允许一次对一个流执行多次操作,所以跳过、标记、重置都很平常。同ByteArrayInputStream和ByteArrayOutputStream一样,该类也有buf和count,buf大小默认是8192。它的作用是额外提供“缓冲功能”以及支持“mark()标记”和“reset()重置方法”。比如BufferedInputStream 会将该输入流的数据分批填入到缓冲区中。每当缓冲区中的数据被读完之后,输入流会再次填充数据缓冲区;如此反复,直到我们读完输入流数据位置。

流操作的特性是数据传输(只能往前进,而不能往后退),而数据传输速度和数据处理的速度存在不平衡,这种不平衡使得数据传输过程中进行缓存处理而释放数据处理器的资源是一种提高程序效率的机制。比如,厕所的抽水马桶,在你上完厕所后,一按冲水则通过水流冲洗干净马桶,通过水流的冲击力来带走你的排泄物,而抽水马桶上方的水槽中的水是一点点进行积累,有时水未积累到位时,按冲水时,会发现马桶冲不干净。在极端情况下,我们不会用一滴滴的水进行冲洗,而是等到水达到一定的量在进行冲马桶。在数据流处理时,CPU也不会等待内存读取数据后就立即处理,而是在内存数据到达一定的量后在进行处理,从而腾出CPU的处理时间。在java.io读写文件时,常常使用缓存进行操作,而不是按部就班的逐个字节读取处理。

BufferedInputStream 的使用比较广泛,用法也不是那么简单,可以参考博客《简书 - 关于BufferedInputStream的理解》《脚本之家 - BufferedInputStream(缓冲输入流)详解》。

BufferedOutputStream 比较简单,只有一个比较独特的方法flush(),用于立即将buf中字节写入流中。

PushbackInputStream

PushbackInputStream 也是一个缓冲流,而且也是FilterInputStream的子类,中文名叫回推输入流。回推就是把读过的字节再放回输入流里,再次read的时候就会再次读到相同字节。

为什么要这样呢?有时需要先读取几个字节以查看将要发生的事情,然后才能确定如何解释当前字节。看一个例子:

try (ByteArrayInputStream is = new ByteArrayInputStream("if (a==1) {a = -1;}".getBytes());
        PushbackInputStream pis = new PushbackInputStream(is);) {
    byte c;
    while ((c = (byte) pis.read()) != -1 ) {
        if (c == '=') {
            c = (byte) pis.read();
            if (c == '=') {
                System.out.print(" 是 ");
            } else {
                System.out.print(" 赋值为 ");
                pis.unread(c);
            }
        } else {
            System.out.print((char)c);
        }
    }
}

这个例子是如果等号后面还是等号就输出“ 是 ”,如果等号后面不是等号,后面的字符回推并将等号输出“ 赋值为 ”,其他原样输出:

if (a 是 1) {a  赋值为  -1;}

SequenceInputStream

这是一个输入流集合,至少要两个流,所以构造方法是public SequenceInputStream(InputStream s1, InputStream s2)public SequenceInputStream(Enumeration<? extends InputStream> e)。SequenceInputStream会依次读取每个流,知道读完。关闭SequenceInputStream也会把相关的所有流关闭。

PrintStream

前面我们已经用过很多次PrintStream了,它就是System.out.print中的那个out。PrintStream的print()和println()支持作用对象类型,如果参数不是基本数据类型,就调用它们的toString()方法。

PrintStream还提供了printf()和format()方法,用于格式化输出内容。可以自行百度。

基本类型数据流 DataInput、DataOutput

这是两个接口,用于对字节和基础数据类型进行转换。主要的实现类是DataInputStream和DataOutputStream,它们分别继承自过滤流。

看例子,猜下输出:

try (ByteArrayOutputStream bao = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bao)) {
    dos.writeInt(88888);
    dos.writeLong(Long.MAX_VALUE - 1);
    dos.writeDouble(2.22);

    System.out.println(bao.toString());

    try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bao.toByteArray()))){
        System.out.println(dis.readInt());
        System.out.println(dis.readLong()); // 可以改成readInt看一下
        System.out.println(dis.readDouble());
    }
}

RandomAccessFile

最后说这个不是InputStream或OutputStream的子类,它实现了三个接口: DataOutput, DataInput, Closeable。RandomAccessFile提供的是对文件内容的随机访问。既然是随机,就需要支持文件内容指针。所以它提供了一个seek(long offset)的方法,用于指定操作读写的位置与文件开头的偏移字节数。

使用RandomAccessFile打开文件需要指定模式,是只读还是可写。设置方法见《CSDN - rws rwd 的区别于联系》。


字节流虽然方便也强大,但是不支持Unicode,前面我们说中文乱码就是这个原因。虽然有其他途径解决字节流支持中文,不过更推荐后面要说的字符流。

Written on December 25, 2019
分类: dev, 标签: java
如果你喜欢,请赞赏! davelet