第1节:InputStream和OutputStream

本文深入解析了Java中的输入输出流概念,通过引入参考系解释了输入输出流的逻辑,详细介绍了InputStream和OutputStream的基本操作及核心方法,如read()、write()、skip()等,帮助读者掌握IO流的正确使用。

在我刚开始学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

<think>我们被要求提供关于Java中FileInputStreamFileOutputStream的使用教程。根据提供的引用内容,我们可以总结出以下关键点: 1. **类概述**: - `FileInputStream` 用于从文件中读取字数据。 - `FileOutputStream` 用于向文件中写入字数据。 2. **基本用法**: - 创建流对象:通过指定文件路径或File对象来创建。 - 读取/写入数据:使用`read()`方法读取一个字(返回-1表示文件结束),使用`write(int b)`方法写入一个字。 - 关闭流:使用`close()`方法关闭流并释放系统资源。 3. **注意事项**: - 使用`FileOutputStream`时,如果文件不存在,会创建新文件;如果文件存在,默认会覆盖原有内容(除非使用追加模式)。 - 追加模式:在创建`FileOutputStream`时,可以传入第二个参数`true`表示追加写入。 4. **异常处理**: - 这些操作可能会抛出`IOException`,因此需要进行异常处理(try-catch或声明抛出)。 5. **示例代码**: - 引用中提供了简单的读取写入示例。 现在,我们将按照以下结构组织教程: 1. 介绍两个类的基本概念用途。 2. 分别讲解FileInputStreamFileOutputStream的构造方法、常用方法。 3. 提供读取写入文件的代码示例。 4. 强调资源关闭的重要性,并介绍try-with-resources语法(Java 7+)来自动关闭资源。 5. 讨论追加写入覆盖写入的区别。 6. 注意事项常见错误。 由于引用中没有提到try-with-resources,但这是现代Java中处理资源的标准方式,我们将补充这一点。 下面开始编写教程:</think>### Java FileInputStream FileOutputStream 使用教程 `FileInputStream` `FileOutputStream` 是 Java I/O 中处理**字流**的核心类,用于读写二进制文件(如图片、音频)或文本文件(需手动编解码)。以下是详细使用指南: --- #### 一、核心功能 | **类** | **功能** | **继承关系** | |---------------------|--------------------------------------|----------------------| | `FileInputStream` | 从文件读取字数据 | `InputStream` → 子类 | | `FileOutputStream` | 向文件写入字数据 | `OutputStream` → 子类 | --- #### 二、基础用法 1. **文件读取(FileInputStream)** ```java try (FileInputStream fis = new FileInputStream("input.txt")) { int byteData; // 逐字读取(返回-1表示文件结束) while ((byteData = fis.read()) != -1) { System.out.print((char) byteData); // 字转字符 } } catch (IOException e) { e.printStackTrace(); } ``` 2. **文件写入(FileOutputStream)** ```java try (FileOutputStream fos = new FileOutputStream("output.txt")) { String text = "Hello Java IO!"; byte[] bytes = text.getBytes(); // 字符串转字数组 fos.write(bytes); // 写入整个数组 } catch (IOException e) { e.printStackTrace(); } ``` --- #### 三、关键特性 1. **覆盖 vs 追加模式** - 覆盖模式(默认):创建流时清空原文件 ```java new FileOutputStream("data.bin"); // 覆盖写入 ``` - 追加模式:添加第二个参数 `true` ```java new FileOutputStream("log.txt", true); // 追加写入[^4] ``` 2. **字缓冲区优化** 避免逐字读写,使用缓冲区提升性能: ```java try (FileInputStream fis = new FileInputStream("source.jpg"); FileOutputStream fos = new FileOutputStream("copy.jpg")) { byte[] buffer = new byte[1024]; // 1KB缓冲区 int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); // 写入实际读取的字数 } } catch (IOException e) { e.printStackTrace(); } ``` --- #### 四、注意事项 1. **资源释放**:必须调用 `close()` 或使用 **try-with-resources**(推荐)释放系统资源[^2]。 2. **异常处理**:所有操作需处理 `IOException`。 3. **文本文件处理**:读写文本时需指定字符集(如 `StandardCharsets.UTF_8`),否则可能乱码。 4. **大文件风险**:避免一次性读取超大文件导致内存溢出。 --- #### 五、典型应用场景 1. 复制二进制文件(图片/视频) 2. 日志文件追加写入 3. 网络传输中的文件分块读写 4. 加密/解密文件的字流处理 > 提示:处理文本文件时,建议使用字符流 `FileReader`/`FileWriter` 避免手动处理编码。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值