第1节:InputStream和OutputStream

在我刚开始学IO的时候,我经常把输入和输出搞混。当时,我认为将数据写到文件应该用输入流,而从文件中读取数据为输出流。因为,这个和我们打字时候的思维很像。我们用键盘在文件里打字的时候,这个动作叫输入;而我们打开文件,读取文件信息的时候,电脑在向我们输出内容。现在,我们知道,这个逻辑是完全错误的。在计算机的世界里,向文件里写数据用输出流,而读取文件内容用输入流。

那么,问题到底出在哪里了呢?为了让自己接受这个逻辑,我引入了一个概念:参考系

我之所以会陷入那个问题,是因为我的参考系不是我自己,而是目标对象。当我输入数据到文件的时候,此时的参考系是文件。数据相对于文件是流入的。而我打开文件,读取内容的时候,数据相对于文件是流出的。而计算机的世界里,恰恰相反。计算机始终认为才是世界的中心。我,指的就是程序本身。当我们以这个思想去理解刚刚那个问题的时候,就可以理解输入输出的概念了。当我在往文件里输入数据的时候,我是数据的发出者,数据是从我这里流出去的,因此写入数据应该用输出流;而当我读取文件的时候,数据是从文件流向我的,因此读取数据应该用输入流。

现实生活中,我们可以用心脏来理解这个问题。我们知道,血液是由静脉流入心脏,心脏处理过后再将动脉血输送到各个器官的。因此,静脉血对于心脏来算,就是输入血流;动脉血就是输出血流。然而对各个器官来讲,动脉血是输入血流,静脉血是输出血流。参考系的不同,输入输出的理解也不同。

输入流


Java提供了InputStream和OutputStream两个抽象类构成了基本的输入输出模型。在InputStream里定义了一个抽象方法read:

public abstract int read() throws IOException;

这个方法可以用来读取一个字节,并且把这个字节作为返回值返回给调用者。如果读到了文件结尾,将返回 -1. 利用这一点,我们可以通过判断读取的返回值是否为-1来判断是否已经读取结束。

Inputstream还定义了许多非抽象方法:

// 读取一段数据到数组,返回实际读入的字节数
public int read(byte b[]) throws IOException {}

// 偏移off个字节后,读取len长度数据到数组,返回实际读入的字节数
public int read(byte b[], int off, int len) throws IOException {}

// 跳过n个字节,返回实际跳过的字节
public long skip(long n) throws IOException {}

其中,上面两个方法都是调用的read()方法,所以实现read()方法不仅在语法上,更是在逻辑上是必须的。

第三个方法我们可以看看其实现:

private static final int MAX_SKIP_BUFFER_SIZE = 2048;

public long skip(long n) throws IOException {

    long remaining = n;
    int nr;

    if (n <= 0) {
            return 0;
    }

    int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
    byte[] skipBuffer = new byte[size];
    while (remaining > 0) {
        nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
        if (nr < 0) {
            break;
        }
        remaining -= nr;
    }

    return n - remaining;
}

首先,你传入一个参数n , 这个n代表的是你需要跳过的字节数。remaining指的是需要处理的字节数。我们发现,它在和MAX_SKIP_BUFFER_SIZE比较取小。也就是说,它最多一次性跳过2048个字节。跳过的原理也就是将这些字节读取出来,这样指针会往前移动。然后它把需要跳过的字节数(remaining)减去实际跳过的字节数(nr),得到还需要处理的字节数。然后将总的字节数减去还需处理的字节数,即已经跳过的字节数。

这里可能要有一个疑问了,为什么不直接返回nr呢?nr不就是实际跳过的字节数吗?没错,理想情况下是可以的,但是我们程序员的逻辑一定要严谨。假如遇到这样的情况,文件已经读到了结尾,此时我们仍要跳过n个字节,在while循环里,nr的值就会变成-1。如果我们把-1返回给调用者是什么意思?往前跳了吗?显然和我们的逻辑是不通的。所以,这里用 n - remaining 并不是多此一举,而是逻辑的严谨性判断。

另外,InputStream还提供了其他的非抽象方法:

// 检查当前可读入的字节数量,返回可读入的字节数量
public int available() throws IOException {}

// 关闭当前流占用
public void close() throws IOException {}

// 为输入流的当前位置打一个标记
public synchronized void mark(int readlimit) {}

// 重置标记
public synchronized void reset() throws IOException {}

// 检查当前流是否支持被标记
public boolean markSupported() {}

// 关闭输入流
public void close() throws IOException {}

mark这个方法很有意思,它一般和reset方法配合使用。我们想理解它的话,需要引入指针的概念。我们每读一个字节,指针都会向前移动一个字节。当我们读到感兴趣的字节时,我们mark一下,继续往后读;读到了某处后reset,指针便会回到mark的位置。

这就好比我们看书。我们假如一本书就是一条数据;每一页就是一个字节;页码就是字节的位置;目光就是指针。我们最开始就是从第1页开始读的,所以指针指在第1页的位置。当我们读到第10页的时候,觉得很有意思,在这插了个书签。这个过程就是mark的过程,参数就是页码。等我们把整章或者整本书读完的时候,我们还想回去看第10页,我们就翻回去。这个过程就是reset的过程。不过,在这里,书签只有一支。也就是说,如果继续在第20页插入书签的话,reset就只能返回到第20页,而不能返回到第10页了。所以,mark具有覆盖的效果。

输出流


前面讲了InputStream,现在讲讲OutputStream。Input作为读的话,Output就作为写存在了。和输入流一样,输出流也定义了一个抽象方法write:

public abstract void write(int b) throws IOException;

这个方法可以向流中写入一个字节。同时它还提供了几个非抽象方法:

// 往流中写入字节数组
public void write(byte b[]) throws IOException {}

// 往流中,从off位置开始,写入len长度数据
public void write(byte b[], int off, int len) throws IOException {}

// 将流中的数据写到目标地址
public void flush() throws IOException {}

// 关闭输出流
public void close() throws IOException {}

其中flush方法很重要。我们调用write方法写入数据,这个时候并没有将数据写入到目的地,只是将数据输送到流里。只有我们调用flush方法后,数据才会被写入到目的地。

举个例子,码头运货。我们将一个个的集装箱看作是一个个的字节,码头的船看作是流。我们write数据的时候,只是将货物从码头运上了船,此时船还没出发呢。只有调用了flush方法后,就开始命令船启动送货。此时的数据才真正的到达目的地。所以我们写入数据需要两个步骤:write -> flush

当然,我们使用完后,还应该及时的关闭流。就好比我们用船送货,在我们用这个码头时,船归我们用。等我们用完了,不及时归还船的话,船就会一直被占用着。其他人如果想用码头的船,就用不到了。如果他要强行用,船主人就不干了:我这船还在用呢,不能给你用。这样不就阻塞了吗?这就是我们常见的IO阻塞。同理,输入流也一样。

输入输出流的家族非常的庞大,我并不想在这里展示它的家族体系,因为没有意义。我想通过一篇讲一个流的方式,最后再总结它们的家族体系,这样思路会清晰的多。

所以,至此我们可以看到,IO流的家族体系是这样的:

IO流家族体系


转载于:https://my.oschina.net/u/3962062/blog/3096832

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值