有关输入输出流(IOStream)的内容,分成两个文档。此篇文档主要介绍节点流。

流(Stream)是对输入输出设备的抽象。在Java中,对于数据的输入和输出操作,都是以“流”的方式进行的。
数据以二进制的形式在程序与设备之间流动传输,就像水通过管道在两个数据池间流动一样。根据流动方向的不同,分为输入流(从设备流向程序)、输出流(从程序流向设备)。这里描述的设备,可以是文件、网络、内存等。

流的分类

可以将数据分拆成流,通过流对象对流进行输入和输出。根据分拆前后的对象,可分为节点流和非节点流。

  • 节点流,是将原本就是字节或字符的数据分拆成字节流或字符流,再输入或输出。因为分拆前后都是相同的数据,不需要进行转换,因此可以直接读取或写入而不必转换。同时,每个设备都是一个具体的节点。
  • 非节点流,与节点流相反,是读取和输出的类型不一致的流,一般不能够直接读取或写入。其中每一种都有一些特殊的功能。

而根据读取数据的形式和输入输出,流可分为以下4个类,位列java.io包内,每种都是一个抽象类。

输入(Input) 输出(Output)
字节(byte) InputStream OutputStream
字符(char) Reader Writer

字符流通常比字节流更快。

根据设备的不同,使用的实现类也不一样。黑色字为节点流,棕色字为非节点流。
字节流(-InputStream/OutputStream):

  • ByteArray-:字节数组。对于输出流,默认一次输出32字节。
  • File-:文件流。
  • Filter-:
    • Buffered-:字节缓冲流。
    • Data-:数据流。
    • Print-(仅输出):快速输出流。(因为可以直接操作文件,也可归为节点流)
  • Object-:对象流,能够将对象进行序列化地输入输出。
  • Piped-:管道流。

字符流(-Reader/Writer):

  • Buffered-:字符缓冲流。
  • CharArray-:字符数组。
  • InputSteam/OutputStream-:字节流转字符流,并指定编码。
    • File-:文件流。
  • Piped-:管道流。
  • Print-(仅输出):快速输出流。(同上)

    使用流

    在代码中,使用流操作数据的的基本步骤是:
  1. 声明流:对流对象的引用进行声明。
  2. 创建流:创建一个流对象,或使用其它类内置的流对象。将引用指向流对象。
  3. 使用流:对流进行使用,使用read()或write()方法将流进行输入或输出。这两个方法有其它两个重载的有参方法。
    • read()方法能够读一个字节/字符,read(byte/char_array[])方法能够读一个数组的字符,read(byte/char_array[], begin_index, lenth)方法能够读数组的指定部分。write同理。
  4. 关闭流:使用流对象的close()方法关闭流。

    输入

    1. public void testByteArrayInputStream() throws Exception {
    2. byte[] buf = "abcdefg".getBytes(); // 将字符串转为byte数组
    3. InputStream input = new ByteArrayInputStream(buf); // 构建流
    4. int result = -1;
    5. while ((result = input.read()) != -1) { // 当读到-1即告结束
    6. System.out.print((char) result); // 一次读一个,挨个打印
    7. }
    8. // 也可以一次读多个字符
    9. byte[] b = new byte[6]; // 一次读6个
    10. int length = 0; // 用于获取读取到的个数
    11. while((length = input.read(b)) != -1) { // 将读取的数据存入到数组b中
    12. System.out.println(length); // 读到的个数
    13. System.out.println(Arrays.toString(b)); // 每次读到的内容
    14. }
    15. // 也可以读特定个
    16. input.read(b, 0, b.lenth) // 从0开始读b.lenth个,存储到b中
    17. input.close(); // 关闭流
    18. }

    输出

    1. public void testByteWriteStream() throws Exception {
    2. OutputStream os = new ByteArrayOutputStream();
    3. InputStream is = new ByteArrayInputStream(
    4. "ABCBoy".getBytes());
    5. // 创建一个字节数组输入流,读取1个就用字节数组输出1个
    6. int result = -1;
    7. byte[] count = new byte[1024]; // 一次读1024个
    8. while((result = is.read(count)) != -1) {
    9. os.write(result); // 向os写入is读取的字节数
    10. os.write(count); // 向os写入一个数组的字节
    11. }
    12. byte[] bs = ((ByteArrayOutputStream)os).toByteArray();
    13. // 将写入os的数据转byte数组
    14. System.out.println(Arrays.toString(bs)); // 打印
    15. }

    使用输入输出流就是使用输入输出流对象。最主要的步骤是:

  5. 将某样的东西转为字节数组/字符数组,如果是控制台之类已经转好了就省略此步

  6. 将数组写入输入流对象,如果是控制台之类自动写入就省略此步
  7. 将输入流对象发送到另一端,如果无需发送就省略此步
  8. 接收输入流对象,将输入流对象转为字节数组/字符数组
  9. 将数组写入输出流对象
  10. 将输出流对象发送到另一端,如果无需发送就省略此步
  11. 接收输出流对象,将输出流对象转为字节数组/字符数组

    管道

    管道是一种抽象概念,可以从管道中读取流数据,以及向管道中写入流数据。读取和写入的节点都是管道。这里的管道倒不是一个对象,就只是把输入输出流对接起来了而已。
    一般使用两个线程来分别管理管道的输入和输出。

    1. public static void main(String[] args) throws Exception {
    2. PipedOutputStream os = new PipedOutputStream(); // 输出流
    3. PipedInputStream is = new PipedInputStream(); // 输入流
    4. is.connect(os); // 管道对接
    5. ExecutorService pool = Executors.newCachedThreadPool(); // 线程池
    6. pool.execute(new Runnable() {
    7. public void run() {
    8. try {
    9. Thread.sleep(2000);
    10. byte[] bytes = "ABCBoy".getBytes();
    11. os.write(bytes, true); // 参数2为ture,写入操作为追加而不是覆盖
    12. os.flush();
    13. } catch (Exception e) { e.printStackTrace(); }
    14. }
    15. });
    16. pool.execute(new Runnable() {
    17. public void run() {
    18. try {
    19. byte[] bytes = new byte[1024];
    20. is.read(bytes);
    21. System.out.print(Arrays.toString(bytes)); // 输出ABCBoy
    22. } catch (IOException e) { e.printStackTrace(); }
    23. }
    24. });
    25. }

    文件

    1. InputStream inputStream = new FileInputStream("src/1.jpg"); // 相对路径,要读取工作目录下的文件
    2. OutputStream os = new FileOutputStream("src/2.jpg"); // 要写入的文件
    3. byte[] bytes = new byte[1024]; // 每次读1024个
    4. int length = -1;
    5. while((length = inputStream.read(bytes)) != -1) {
    6. os.write(bytes, 0, length);
    7. }
    8. os.flush(); // 将流中的数据强制刷新到目标文件中。每次写入数据都要进行flush
    9. inputStream.close();
    10. os.close();

    文件操作:

  • exists():判断文件是否存在
  • getAbsolutePath():获取绝对路径
  • getPath():获取相对路径(相对路径位于工作目录下)
  • createNewFile():若文件路径为相对路径,则在项目目录下创建文件,且不会创建不存在的文件夹
  • mkdir():只创建一个文件夹,如果路径不存在则不创建
  • mkdirs():层级创建文件夹
  • deleteOnExit():删除最底层的文件或文件夹
  • 输出流写入的文件若不存在则创建;输入流读取的文件不存在则报错。

    异常处理

    流在使用时会产生IOException需要处理;文件在使用时也可能会产生异常需要处理。一般的处理方式有以下两种:
  1. 将异常向上抛出。
  2. 使用try-catch块包裹。但这又会带来一个问题——流若定义在try块中,若出现异常就会跳过后续代码,导致流无法关闭。因此需要把流定义在try外,把关闭流放在finally块中——而关闭流又会抛异常,又要处理空指针的问题。于是处理异常就会变得十分繁琐:
    1. Reader in = null; // 定义放在try外,以便try的使用和finally的关闭
    2. try {
    3. in = new FileReader("D:/1.txt");
    4. in.read();
    5. } catch (IOException e) {
    6. e.printStackTrace();
    7. } finally {
    8. if(in != null) { // 处理空指针,进行判空处理
    9. try {
    10. in.close(); // 关闭流会抛异常,再用一次try-catch
    11. } catch (IOException e) {
    12. e.printStackTrace();
    13. }
    14. }
    15. }
    在JDK1.7之后,引入了try-with-resource资源自动释放机制,使得资源在try-catch运行结束后会自动释放:
    1. try(Reader in = new FileReader("D:/1.txt")) {
    2. in.read();
    3. } catch (IOException e) {
    4. e.printStackTrace();
    5. }
    其中try后面括号内的为with语法,能够容纳资源的声明和初始化,多条语句使用分号隔开。