Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • Java入门基础

  • Java核心基础

    • 多线程

    • Java常用类

    • 枚举类与注解

    • Java集合

    • 数据结构与算法

    • 泛型

    • IO流

      • IO流概述
      • File对象
      • InputStream
        • 缓冲
        • 阻塞
        • ByteArrayInputStream
        • BufferedInputStream
        • 小结
      • OutputStream
      • Filter模式
      • 操作Zip
      • 读取classpath资源
      • 序列化
      • Reader
      • Writer
      • 转换流
      • PrintStream和PrintWriter
      • RandomAccessFile
      • Files工具类
    • 网络编程

    • 反射

    • 函数式编程

  • 设计模式

  • Web开发

  • SpringBoot

  • 微服务

  • Elasticsearch

  • 运维

  • 后端
  • Java核心基础
  • IO流
SaulJWu
2021-01-07

InputStream

InputStream就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。

要特别注意的一点是,InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read(),签名如下:

public abstract int read() throws IOException;
1

这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。

FileInputStream是InputStream的一个子类。顾名思义,FileInputStream就是从文件流中读取数据。下面的代码演示了如何完整地读取一个FileInputStream的所有字节:

public void readFile() throws IOException {
    // 创建一个FileInputStream对象:
    InputStream input = new FileInputStream("src/readme.txt");
    for (;;) {
        int n = input.read(); // 反复调用read()方法,直到返回-1
        if (n == -1) {
            break;
        }
        System.out.println(n); // 打印byte的值
    }
    input.close(); // 关闭流
}
1
2
3
4
5
6
7
8
9
10
11
12

在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的。应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时地关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行。

InputStream和OutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源。

我们还要注意到在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException。

仔细观察上面的代码,会发现一个潜在的问题:如果读取过程中发生了IO错误,InputStream就没法正确地关闭,资源也就没法及时释放。

因此,我们需要用try ... finally来保证InputStream在无论是否发生IO错误的时候都能够正确地关闭:

public void readFile() throws IOException {
    InputStream input = null;
    try {
        input = new FileInputStream("src/readme.txt");
        int n;
        while ((n = input.read()) != -1) { // 利用while同时读取并判断
            System.out.println(n);
        }
    } finally {
        if (input != null) { input.close(); }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

用try ... finally来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源。推荐的写法如下:

public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println(n);
        }
    } // 编译器在此自动为我们写入finally并调用close()
}
1
2
3
4
5
6
7
8

实际上,编译器并不会特别地为InputStream加上自动关闭。

编译器只看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。

InputStream和OutputStream都实现了这个接口,因此,都可以用在try(resource)中。

# 缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。

InputStream提供了两个重载方法来支持读取多个字节:

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数

利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。

read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。

利用缓冲区一次读取多个字节的代码如下:

public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        // 定义1000个字节大小的缓冲区:
        byte[] buffer = new byte[1000];
        int n;
        while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
            System.out.println("read " + n + " bytes.");
        }
    }
}
1
2
3
4
5
6
7
8
9
10

# 阻塞

在调用InputStream的read()方法读取数据时,我们说read()方法是阻塞(Blocking)的。

它的意思是,对于下面的代码:

int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
1
2
3

执行到第二行代码时,必须等read()方法返回后才能继续。

因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。

# ByteArrayInputStream

用FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。

此外,ByteArrayInputStream可以在内存中模拟一个InputStream:

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] data = { 72, 101, 108, 108, 111, 33 };
        try (InputStream input = new ByteArrayInputStream(data)) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println((char)n);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

运行结果:

H
e
l
l
o
!
1
2
3
4
5
6

ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream。

举个例子:我们想从文件中读取所有字节,并转换成char然后拼成一个字符串,可以这么写:

public class Main {
    public static void main(String[] args) throws IOException {
        String s;
        try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
            int n;
            StringBuilder sb = new StringBuilder();
            while ((n = input.read()) != -1) {
                sb.append((char) n);
            }
            s = sb.toString();
        }
        System.out.println(s);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

要测试上面的程序,就真的需要在本地硬盘上放一个真实的文本文件。

如果我们把代码稍微改造一下,提取一个readAsString()的方法:

public class Main {
    public static void main(String[] args) throws IOException {
        String s;
        try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
            s = readAsString(input);
        }
        System.out.println(s);
    }

    public static String readAsString(InputStream input) throws IOException {
        int n;
        StringBuilder sb = new StringBuilder();
        while ((n = input.read()) != -1) {
            sb.append((char) n);
        }
        return sb.toString();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

对这个String readAsString(InputStream input)方法进行测试就相当简单,因为不一定要传入一个真的FileInputStream:

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] data = { 72, 101, 108, 108, 111, 33 };
        try (InputStream input = new ByteArrayInputStream(data)) {
            String s = readAsString(input);
            System.out.println(s);
        }
    }

    public static String readAsString(InputStream input) throws IOException {
        int n;
        StringBuilder sb = new StringBuilder();
        while ((n = input.read()) != -1) {
            sb.append((char) n);
        }
        return sb.toString();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行结果:

Hello!
1

这就是面向抽象编程原则的应用:接受InputStream抽象类型,而不是具体的FileInputStream类型,从而使得代码可以处理InputStream的任意实现类。

# BufferedInputStream

BufferedInputStream也是InputStream的一个实现类,但它是一个缓冲流,就算你使用FileInputStream的效率也不如BufferedInputStream,用法也是一样的,所以我们开发中应该优先使用缓冲流。

# 小结

Java标准库的java.io.InputStream定义了所有输入流的超类:

  • FileInputStream实现了文件流输入;
  • ByteArrayInputStream在内存中模拟一个字节流输入。

总是使用try(resource)来保证InputStream正确关闭。

帮我改善此页面 (opens new window)
#InputStream#读取#输入流
上次更新: 2021/01/09, 11:11:31
File对象
OutputStream

← File对象 OutputStream→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式