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) ; // 输出内容
}
}
}
}
程序运行结果,如下所示:

- 示例二,读取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();
}
}
程序运行结果,如下所示:

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


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();
}
}
程序运行结果,如下所示:

- 示例二,编译源代码时,需要识别并跳过注释部分,只处理有效代码。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();
}
}
程序运行结果,如下所示:

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();
}
}
}
}
程序运行结果,如下所示:

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文件,内容如下:

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

- 示例二,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文件,内容如下:

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

七、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有很多子类,如下:

其中最常用的是 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文件,内容如下:

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

- 示例二,可复用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();
}
}
}
程序运行结果,如下所示:

7.2、abstract Writer.class
abstract Writer.class有很多子类,如下:

其中最常用的是 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文件,内容如下:

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

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

将一组字符写入到 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);
}
}

浙公网安备 33010602011771号