2、Java的IO概览(二)

阅读本文前请先阅读我的另一篇博客1、Java的IO概览(一)

六、解析输入数据的Stream

6.1、PushbackInputStream

  PushbackInputStream可以将已读取的字节(byte)重新推回输入流中。这样,下次调用read()函数时,这些字节(byte)就会再次被读取。因此,PushbackInputStream可以从输入流中解析字节(byte)数据。

  • 示例一,读取String类型的数据
package com.xxx.StreamAndReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.PushbackInputStream;
public class StreamTest {
   public static void main(String[] args) throws IOException {
      String str = "www.mldnjava.cn" ;      // 定义字符串
      PushbackInputStream push = null ;     // 定义回退流对象
      ByteArrayInputStream bai = null ;     // 定义内存输入流
      bai = new ByteArrayInputStream(str.getBytes()) ;   // 实例化内存输入流
      push = new PushbackInputStream(bai) ;  // 从内存中读取数据
      System.out.print("读取之后的数据为:") ;
      int temp = 0 ;
      while((temp=push.read())!=-1){ // 读取内容
         if(temp=='.'){ // 判断是否读取到了“.”
            push.unread(temp) ;    // 放回到缓冲区之中
            temp = push.read() ;   // 再读一遍
            System.out.print("(退回"+(char)temp+")") ;
         }else{
            System.out.print((char)temp) ; // 输出内容
         }
      }
   }
}

程序运行结果,如下所示:
clipboard

  • 示例二,读取byte[]数组类型的数据
package com.xxx.StreamAndReader;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.PushbackInputStream;
public class StreamTest {
   public static void main(String[] args) throws IOException {
      byte[] inputBytes = {1,2,3,4,5,6,7,8,9,10};
      ByteArrayInputStream bais = new ByteArrayInputStream(inputBytes);
      PushbackInputStream pbis = new PushbackInputStream(bais, 10);
      pbis.unread(inputBytes, 0, 3);
      pbis.unread(inputBytes, 6, 3);

      byte[] outputBytes1 = new byte[10];
      pbis.read(outputBytes1, 0, 5);
      byte[] outputBytes2 = new byte[10];
      pbis.read(outputBytes2, 0, 8);
      byte[] outputBytes3 = new byte[10];
      pbis.read(outputBytes3, 0, 9);

      System.out.println("outputBytes1中的内容为:");
      for (int i = 0; i < outputBytes1.length; i++) {
         System.out.print(outputBytes1[i]);
      }
      System.out.println();

      System.out.println("outputBytes2中的内容为:");
      for (int i = 0; i < outputBytes2.length; i++) {
         System.out.print(outputBytes2[i]);
      }
      System.out.println();
      System.out.println("outputBytes3中的内容为:");
      for (int i = 0; i < outputBytes3.length; i++) {
         System.out.print(outputBytes3[i]);
      }
      System.out.println();
   }
}

程序运行结果,如下所示:
clipboard

PushbackInputStream中的缓冲区(buf缓冲数组)和ByteArrayInputStream 构造函数输入的inputBytes数组,在执行PushbackInputStream.class::unread()函数、PushbackInputStream.class::read函数时的关系,如下所示:
clipboard
clipboard

6.2、PushbackReader

  PushbackReader可以将已读取的字符(char)重新推回输入流中。这样,下次调用read()函数时,这些字符(char)就会再次被读取。因此,PushbackReader可以从输入流中解析字符(char)数据。

  • 示例一,在解析文本或代码时,常需要查看下一个字符来决定如何处理当前内容,但又不希望真正消耗该字符。
package com.xxx.StreamAndReader;
import java.io.*;
public class PushbackReaderTest {
   public static void main(String[] args) throws IOException {
      String input = "if (x > 5) { System.out.println(\"Hello\"); }";
      StringReader reader = new StringReader(input);
      PushbackReader pushbackReader = new PushbackReader(reader);
      int c;
      while ((c = pushbackReader.read()) != -1) {
         if (c == '/') {
            int next = pushbackReader.read();
            if (next == '/') {
               // 遇到单行注释,跳过到行尾
               while ((c = pushbackReader.read()) != -1 && c != '\n') {}
            } else {
               // 不是注释,将字符推回
               pushbackReader.unread(next);
               System.out.print((char) c);
            }
         } else {
            System.out.print((char) c);
         }
      }
      pushbackReader.close();
   }
}

程序运行结果,如下所示:
clipboard

  • 示例二,编译源代码时,需要识别并跳过注释部分,只处理有效代码。PushbackReader 可用于识别 // 或 /* */ 注释,并在识别后将非注释字符推回,以便继续处理。
package com.xxx.StreamAndReader;
import java.io.*;
public class PushbackReaderTest {
   public static void main(String[] args) throws IOException {
      String source = "public class Test {\n" +
            "    // This is a comment\n" +
            "    System.out.println(\"Hello\");\n" +
            "}";
      StringReader reader = new StringReader(source);
      PushbackReader pushbackReader = new PushbackReader(reader);
      int c;
      while ((c = pushbackReader.read()) != -1) {
         if (c == '/') {
            int next = pushbackReader.read();
            if (next == '/') {
               // 跳过单行注释
               while ((c = pushbackReader.read()) != -1 && c != '\n') {}
            } else if (next == '*') {
               // 跳过多行注释
               boolean foundEnd = false;
               while ((c = pushbackReader.read()) != -1 && !foundEnd) {
                  if (c == '*') {
                     int next2 = pushbackReader.read();
                     if (next2 == '/') {
                        foundEnd = true;
                     } else {
                        pushbackReader.unread(next2);
                     }
                  }
               }
            } else {
               // 不是注释,推回并输出
               pushbackReader.unread(next);
               System.out.print((char) c);
            }
         } else {
            System.out.print((char) c);
         }
      }
      pushbackReader.close();
   }
}

程序运行结果,如下所示:
clipboard

6.3、StreamTokenizer
  能够将从 Reader 中读取的字符转换为标记。例如,在字符串“Mary had a little lamb”中,每个单词都是一个独立的标记。在解析文件或计算机语言时,通常会将输入内容分解为一个个“标记”,然后再对其进行进一步处理(词法分析)。如下所示:

package com.xxx.StreamAndReader;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
public class StreamTokenizerTest {
   public static void main(String[] args) throws IOException {
      StreamTokenizer streamTokenizer = new StreamTokenizer(
            new StringReader("Mary had 1 little lamb..."));
      while(streamTokenizer.nextToken() != StreamTokenizer.TT_EOF){

         if(streamTokenizer.ttype == StreamTokenizer.TT_WORD) {
            System.out.println(streamTokenizer.sval);
         } else if(streamTokenizer.ttype == StreamTokenizer.TT_NUMBER) {
            System.out.println(streamTokenizer.nval);
         } else if(streamTokenizer.ttype == StreamTokenizer.TT_EOL) {
            System.out.println();
         }
      }
   }
}

程序运行结果,如下所示:
clipboard

6.4、LineNumberReader
  LineNumberReader是一种缓冲读取器,它能够记录所读取字符的行号,行编号从 0 开始计数。每当 LineNumberReader 在被包裹的 Reader 返回的字符中遇到行终止符时,行号就会增加。

  • 示例一,readLine()函数、getLineNumber()函数、setLineNumber()函数
package com.xxx.StreamAndReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
public class StreamTest {
   public static void main(String[] args) throws FileNotFoundException {
      FileReader fr = new FileReader("D:\\nio-data.txt");
      LineNumberReader reader = new LineNumberReader(fr);
      String line;
      try {
         while ((line = reader.readLine()) != null) {
            if ("一切,都在意料之中。".equals(line)){
               reader.setLineNumber(6);
            }
            System.out.println(reader.getLineNumber() + ":" + line);
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

我的windows操作系统的D盘根目录下有nio-data.txt文件,内容如下:
clipboard

程序运行结果,如下所示:
clipboard

  • 示例二,skip()函数
package com.xxx.StreamAndReader;
import java.io.*;
public class StreamTest {
   public static void main(String[] args) throws FileNotFoundException {
      FileReader fr = new FileReader("D:\\nio-data.txt");
      LineNumberReader reader = new LineNumberReader(fr);
      String line;
      try {
         while ((line = reader.readLine()) != null) {
            System.out.println(reader.getLineNumber() + ":" + line);
            reader.skip(3);//从下一行开始,每一行跳过前3个字符
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

我的windows操作系统的D盘根目录下有nio-data.txt文件,内容如下:
clipboard

程序运行结果,如下所示:
clipboard

七、Reader和Writer

  java.io.Reader和 java.io.Writer的运作方式与 InputStream 和 OutputStream 类似,但不同之处在于 Reader 和 Writer 是基于字符(char,JVM中使用Unicode编码,char占2个byte)的IO流(主要用于读取和写入中文文本)。而 InputStream 和 OutputStream 是基于字节(byte)的。

7.1、abstract Reader.class

  abstract Reader.class有很多子类,如下:
clipboard

其中最常用的是 BufferedReader, PushbackReader, InputStreamReader, StringReader;

7.1.1、BufferedReader

  java.io.BufferedReade提供了缓冲功能。缓冲能够显著提高 I/O 的处理速度。与每次从底层读取一个字符不同,BufferedReade 会一次性读取较大的数据块(数组)。这种方式通常要快得多,尤其是在进行磁盘访问和处理较大数据量时。
  BufferedReader 类似于 BufferedInputStream ,但它们并不相同。BufferedReader 读取的是字符(char,JVM中使用Unicode编码,char占2个byte),而 BufferedInputStream 读取的是字节(byte)。

  • 示例一,readLine()函数、read()函数
package com.xxx.StreamAndReader;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReaderTest {
   public static void main(String[] args) throws FileNotFoundException {
      try (BufferedReader br = new BufferedReader(new FileReader("D:\\nio-data.txt"))) {
         String line;
         while ((line = br.readLine()) != null) {
            System.out.println(line);
         }
      } catch (IOException e) {
         e.printStackTrace();
      }

      try (BufferedReader br = new BufferedReader(new FileReader("D:\\nio-data.txt"))) {
         int character;
         while ((character = br.read()) != -1) {
            System.out.print((char) character);
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
      System.out.println();//换行

      try (BufferedReader br = new BufferedReader(new FileReader("D:\\nio-data.txt"))) {
         char[] buffer = new char[1024];
         int length;
         while ((length = br.read(buffer)) != -1) {
            System.out.print(new String(buffer, 0, length));
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

我的windows操作系统的D盘根目录下有nio-data.txt文件,内容如下:
clipboard

程序运行结果,如下所示:
clipboard

  • 示例二,可复用char[]数组缓冲区的BufferedReader
      标准的BufferedReader 的一个缺点是它只能使用一次。一旦执行了BufferedReader.class::close()函数后,就无法再使用了。如果需要读取大量的文件或网络流,就必须为每个要读取的文件或网络流创建一个新的 BufferedReader。这意味着要创建新的对象,更重要的是,还要创建一个新的char[]数组作为 BufferedReader 内部的缓冲区。如果要读取的文件或流的数量很多,并且它们是连续快速地读取的,那么这可能会给 Java 垃圾回收器带来压力。因此,可以自己实现一个ReusableBufferedReader,相比于JDK提供的BufferedReader,自己实现的ReusableBufferedReader 内部的char[]数组缓冲区能够被重复使用。如下所示:
import java.io.IOException;
import java.io.Reader;

public class ReusableBufferedReader extends Reader {

    private char[]      buffer = null;
    private int         writeIndex = 0;//写指针
    private int         readIndex  = 0;//读指针
    private boolean     endOfReaderReached = false;

    private Reader      source = null;

    
    public ReusableBufferedReader(char[] buffer) {
        this.buffer = buffer;
    }

    public ReusableBufferedReader setSource(Reader source){
        this.source = source;
        this.writeIndex = 0;
        this.readIndex  = 0;
        this.endOfReaderReached = false;
        return this;
    }

    @Override
    public int read() throws IOException {
        if(endOfReaderReached) {
            return -1;
        }

        if(readIndex == writeIndex) {
            if(writeIndex == buffer.length) {
                this.writeIndex = 0;
                this.readIndex  = 0;
            }
            //data should be read into buffer.
            int bytesRead = readCharsIntoBuffer();
            while(bytesRead == 0) {
                //continue until you actually get some bytes !
                bytesRead = readCharsIntoBuffer();
            }

            //if no more data could be read in, return -1;
            if(bytesRead == -1) {
                return -1;
            }
        }

        return 65535 & this.buffer[readIndex++];
    }

    @Override
    public int read(char[] dest, int offset, int length) throws IOException {
        int charsRead = 0;
        int data = 0;
        while(data != -1 && charsRead < length){
            data = read();
            if(data == -1) {
                endOfReaderReached = true;
                if(charsRead == 0){
                    return -1;
                }
                return charsRead;
            }
            dest[offset + charsRead] = (char) (65535 & data);
            charsRead++;
        }
        return charsRead;
    }

    private int readCharsIntoBuffer() throws IOException {
        int charsRead = this.source.read(this.buffer, this.writeIndex, this.buffer.length - this.writeIndex);
        writeIndex += charsRead;
        return charsRead;
    }

    @Override
    public void close() throws IOException {
        this.source.close();
    }
}

使用方式如下(伪代码):

//创建了一个可复用的 ReusableBufferedReader 对象,
//其内部缓冲区为一个大小为 2MB 的字符数组(1024 * 1024 个字符,每个字符占用 2 个字节)
ReusableBufferedReader reusableBufferedReader =
    new ReusableBufferedReader(new char[1024 * 1024]);
    
//设置数据源
FileReader reader = new FileReader("xxx.txt");
reusableBufferedReader.setSource(reader);

//如果需要更换源Reader,可以执行close()函数,然后再重新执行setSource()函数,如下所示:
reusableBufferedReader.setSource(new FileReader("xxx1.txt"));
reusableBufferedReader.close();
reusableBufferedReader.setSource(new FileReader("xxx2.txt"));
7.1.2、PushbackReader

  请看本文标题6.2

7.1.3、InputStreamReader

  阅读我的另一篇博客1、Java的IO概览(一)的标题2.1

7.1.4、StringReader

  Java 中用于读取字符串的字符流Reader。如下所示:

package com.xxx.StreamAndReader;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

public class StringReaderTest {
   public static void main(String[] args){
      // 模拟从文件中读取的文本数据
      String data = "Alice 90\r\nBob 85\r\nCharlie 95";
      StringReader stringReader = new StringReader(data);
      BufferedReader reader = new BufferedReader(stringReader);
      try {
         String line;
         while ((line = reader.readLine()) != null) {
            // 按照空格分割每一行的数据,第一个元素是学生姓名,第二个元素是成绩
            String[] parts = line.split(" ");
            String studentName = parts[0];
            int score = Integer.parseInt(parts[1]);
            System.out.println("Student: " + studentName + ", Score: " + score);
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

程序运行结果,如下所示:
image

7.2、abstract Writer.class

  abstract Writer.class有很多子类,如下:
clipboard
其中最常用的是 BufferedWriter, FileWriter, OutputStreamWriter, StringWriter;

7.2.1、BufferedWriter

   java.io.BufferedWriter为 继承了Writer.class 的其它实例提供了缓冲功能。缓冲能够显著提高 I/O 操作的速度。与每次向网络或磁盘写入一个字符不同,BufferedWriter 会一次性写入较大的数据块。尤其是在磁盘访问和较大数据量的情况下,这种方式通常要快得多。使用如下所示:

package com.xxx.StreamAndReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterTest {
   public static void main(String[] args) throws IOException {
      // 创建一个文件写入流
      FileWriter fileWriter = new FileWriter("D:\\nio-data.txt");
      // 创建一个缓冲区写入流,缓冲区大小是1MB(因为JVM是unicode编码,所以1个字符占2个byte,512个字符是1kb的大小,1MB=1kb*1024
      BufferedWriter bufferedWriter = new BufferedWriter(fileWriter,512*1024);
      // 写入文本数据
      bufferedWriter.write("hello world!!!");
      // 刷新缓冲区
      bufferedWriter.flush();
      // 关闭缓冲区写入流和文件写入流
      bufferedWriter.close();
      fileWriter.close();
   }
}

我的windows操作系统的D盘根目录下有nio-data.txt文件,内容如下:
clipboard

程序运行后,nio-data.txt文件的内容如下:
clipboard

7.2.2、OutputStreamWriter

  java.io.OutputStreamWriter主要用于对 OutputStream进行封装,从而将基于字节(byte)的输出流转换为基于字符(char)的输出流。同时,OutputStreamWriter还可以对任何 OutputStream的子类进行封装。
  OutputStreamWriter 还可以指定输出流的任意编码方式(例如以 UTF-8 或 UTF-16 编码方式),OutputStreamWriter支持的编码方式有以下类型(此处只展示部分,都是abstract Charset.class的子类):
clipboard

  将一组字符写入到 Java 的 OutputStreamWriter 中要比逐个字符地写要快得多。因此,建议在可能的情况下尽可能使用OutputStreamWriter .class::write(char[]) 函数。以下是2个OutputStreamWriter使用的场景,具体哪种使用方式的性能更高,需要根据实际场景来测试。

  • 示例一,OutputStreamWriter与BufferedWriter和FileOutputStream配合使用
//伪代码
int bufferSize = 8 * 1024;
Writer writer =
    new BufferedWriter(
          new OutputStreamWriter(
             new FileOutputStream("xxx.txt"),
            "UTF-8"
          ),
          bufferSize
    );
  • 示例二,OutputStreamWriter与BufferedOutputStream和FileOutputStream配合使用
//伪代码
int bufferSize = 8 * 1024;
OutputStreamWriter outputStreamWriter =
    new OutputStreamWriter(
        new BufferedOutputStream(
              new FileOutputStream("xxx.txt"),
              bufferSize
        ),
        "UTF-8"
    );
7.2.3、StringWriter

  与StringBuffer的使用方式类似。

  • 示例一,构建字符串
import java.io.StringWriter;

public class Example {
    public static void main(String[] args) {
        StringWriter sw = new StringWriter();
        sw.write("Hello");
        sw.append(" ");
        sw.write("World!");
        String result = sw.toString();
        System.out.println(result); // 输出: Hello World!
    }
}
  • 示例二,如果预知数据量较大,可指定初始容量以减少内存重新分配。
StringWriter sw = new StringWriter(100); // 初始缓冲区大小为100个字符
sw.write("这是一个较长的字符串...");
String result = sw.toString();
  • 示例三,获取底层的StringBuffer进行操作
StringWriter sw = new StringWriter();
sw.write("Hello");
StringBuffer buffer = sw.getBuffer();
buffer.insert(5, " Java"); // 在索引5处插入 " Java"
String result = sw.toString();
System.out.println(result); // 输出: Hello Java

八、并发和IO流的关系

  在同一个时间,从 InputStream or Reader 中读取数据的线程只能是1个;同一个时间,向OutputStream or Writer中写入数据的操作也不应超过一个线程。

九、IO中的异常处理

  初始化流的代码必须用try-catch-finally包裹,并且在finally中将流关闭,如下所示:

InputStream input = null;

try{
  input = new FileInputStream("c:\\data\\input-text.txt");

  int data = input.read();
  while(data != -1) {
      //do something with data...
      //doSomethingWithData() 方法内部抛出异常导致当前线程中断,也会执行finally块中的代码,关闭流
      doSomethingWithData(data);

      data = input.read();
  }
}catch(IOException e){
  //do something with e... log, perhaps rethrow etc.
} finally {
    try{
    if(input != null) input.close();
    } catch(IOException e){
        //此处需要提醒使用者,流关闭失败了
        logger.error("FileInputStream closing failed", e);
    }
}
posted @ 2026-02-03 08:12  Carey_ccl  阅读(4)  评论(0)    收藏  举报