关于BufferedInputStream类的mark与reset方法的一个bug的问题

探讨了Java中BufferedInputStream类的mark与reset方法在特定条件下触发的异常问题,并提出了三种解决方案。

关于BufferedInputStream类的mark与reset方法的一个bug的问题.txt
sylilzy@gmail.com 施祖阳 http://www.shizuyang.cn
2010-7-7 13:20:26 星期三

最近在调试程序时发现BufferedInputStream类的mark与reset方法存在一个bug,重现方式如下:

import java.io.BufferedInputStream;
import java.io.FileInputStream;

public class BugTest {
/**
* @param args
*/
static public void main(String[] p) throws Exception {
   BufferedInputStream bis = new BufferedInputStream(new FileInputStream("e://temp//a.txt"));
   int available = bis.available();
   System.out.println("available:" + available);
   // ---
   int markLimit = available;
   bis.mark(markLimit);
   System.out.println("---mark:" + markLimit);
   byte[] temp = new byte[available];
   bis.read(temp);
   System.out.println("---read:" + available);
   System.out.println("---read one byte:" + bis.read());
   bis.reset();
   System.out.println("reset ok!");
   bis.close();
}
}

在以上类运行时,如果e://temp//a.txt文件的大小在8192字节以上,则运行结果如下:
available:8384
---mark:8384
---read:8384
---read one byte:-1
Exception in thread "main" java.io.IOException: Resetting to invalid mark
at java.io.BufferedInputStream.reset(Unknown Source)
at com.BugTest.main(BugTest.java:22)

如果e://temp//a.txt文件的大小在8192字节以下,则不会有此异常。

为了分析问题产生的原因,我写了如下一个类来显示BufferedInputStream类的相关信息:


import java.io.BufferedInputStream;
import java.io.InputStream;

public class MyBuffer extends BufferedInputStream {
public MyBuffer(InputStream in, int size) {
   super(in, size);
   // TODO Auto-generated constructor stub
}

public MyBuffer(InputStream in) {
   super(in);
   // TODO Auto-generated constructor stub
}

public int getMarklimit() {
   return marklimit;
}

public int getMarkPosition() {
   return markpos;
}

public int getBufferLength() {
   return buf.length;
}
}

测试类修改如下:

import java.io.*;
import java.util.regex.Pattern;

/**
* @author shizy TODO To change the template for this generated type comment go
*         to Window - Preferences - Java - Code Style - Code Templates
*/
public class Test {
static public void main(String[] p) throws Exception {
   MyBuffer bis = new MyBuffer(new FileInputStream("e://temp//a.txt"));
   int available = bis.available();
   System.out.println("available:" + available);
   System.out.println("getBufferLength:" + bis.getBufferLength());
   System.out.println("getMarklimit:" + bis.getMarklimit());
   System.out.println("getMarkPosition:" + bis.getMarkPosition());
   // ---
   int markLimit = available;
   bis.mark(markLimit);
   System.out.println("---mark:" + markLimit);
   System.out.println("getBufferLength:" + bis.getBufferLength());
   System.out.println("getMarklimit:" + bis.getMarklimit());
   System.out.println("getMarkPosition:" + bis.getMarkPosition());
   byte[] temp = new byte[available];
   bis.read(temp);
   System.out.println("---read:" + available);
   System.out.println("getBufferLength:" + bis.getBufferLength());
   System.out.println("getMarklimit:" + bis.getMarklimit());
   System.out.println("getMarkPosition:" + bis.getMarkPosition());
   System.out.println("---read one byte:" + bis.read());
   System.out.println("getBufferLength:" + bis.getBufferLength());
   System.out.println("getMarklimit:" + bis.getMarklimit());
   System.out.println("getMarkPosition:" + bis.getMarkPosition());
   bis.reset();
   System.out.println("reset ok!");
   bis.close();
}
}

则运行结果如下:
available:8384
getBufferLength:8192
getMarklimit:0
getMarkPosition:-1
---mark:8384
getBufferLength:8192
getMarklimit:8384
getMarkPosition:0
---read:8384
getBufferLength:8384
getMarklimit:8384
getMarkPosition:0
---read one byte:-1
getBufferLength:8384
getMarklimit:8384
getMarkPosition:-1
Exception in thread "main" java.io.IOException: Resetting to invalid mark
at java.io.BufferedInputStream.reset(Unknown Source)
at com.Test.main(Test.java:41)

可见,在输出了“---read:8384”后,BufferLength变成了与Marklimit一样的大小,但在"---read one byte:-1"后,MarkPosition变成了-1,导致了reset时出错。

解决办法有三种:
1.增加BufferedInputStream的BufferLength,在上例中,增加到8385则可解决问题。但此大小需要由输入流的数据量解定,此方法并不太可行。
2.改变markLimit大小。将int markLimit = available;修改为:int markLimit = available+1;。采用这种方式,则上例运行结果如下:
available:8384
getBufferLength:8192
getMarklimit:0
getMarkPosition:-1
---mark:8385
getBufferLength:8192
getMarklimit:8385
getMarkPosition:0
---read:8384
getBufferLength:8385
getMarklimit:8385
getMarkPosition:0
---read one byte:-1
getBufferLength:8385
getMarklimit:8385
getMarkPosition:0
reset ok!

3.不要在流数据结束之后再调用bis.read方法,这种方法并不能完全保证不出问题,因为流数据是否结束,必须要通过read的返回值才能知道。

看来,此问题的出现情况是当BufferLength刚好与从输入流读入的数据相等时(缓冲区会随着marklimit大小增长),如果再读入数据,即时此时未读入数据,则也会认为超过了Marklimit限制,则reset会抛出异常。

注:
jdk版本:
java version "1.6.0_05"
Java(TM) SE Runtime Environment (build 1.6.0_05-b13)
Java HotSpot(TM) Client VM (build 10.0-b19, mixed mode, sharing)

package com.zzyl.common.utils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import java.io.File; import java.io.IOException; import java.io.InputStream; public class PDFUtil { public static String pdfToString(InputStream inputStream) { PDDocument document = null; try { // 加载PDF文档 document = PDDocument.load(inputStream); // 创建一个PDFTextStripper实例来提取文本 PDFTextStripper pdfStripper = new PDFTextStripper(); // 从PDF文档中提取文本 String text = pdfStripper.getText(document); return text; } catch (IOException e) { e.printStackTrace(); } finally { // 关闭PDF文档 if (document != null) { try { document.close(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } } java.io.EOFException: null at org.apache.fontbox.ttf.TTFDataStream.readUnsignedInt(TTFDataStream.java:151) at org.apache.fontbox.ttf.TTFParser.readTableDirectory(TTFParser.java:312) at org.apache.fontbox.ttf.TTFParser.parse(TTFParser.java:139) at org.apache.fontbox.ttf.TTFParser.parse(TTFParser.java:87) at org.apache.pdfbox.pdmodel.font.FileSystemFontProvider.addTrueTypeFont(FileSystemFontProvider.java:657) at org.apache.pdfbox.pdmodel.font.FileSystemFontProvider.scanFonts(FileSystemFontProvider.java:379) at org.apache.pdfbox.pdmodel.font.FileSystemFontProvider.<init>(FileSystemFontProvider.java:358) at org.apache.pdfbox.pdmodel.font.FontMapperImpl$DefaultFontProvider.<clinit>(FontMapperImpl.java:140) at org.apache.pdfbox.pdmodel.font.FontMapperImpl.getProvider(FontMapperImpl.java:159) at org.apache.pdfbox.pdmodel.font.FontMapperImpl.findFont(FontMapperImpl.java:423) at org.apache.pdfbox.pdmodel.font.FontMapperImpl.getTrueTypeFont(FontMapperImpl.java:331) at org.apache.pdfbox.pdmodel.font.PDTrueTypeFont.<init>(PDTrueTypeFont.java:215) at org.apache.pdfbox.pdmodel.font.PDFontFactory.createFont(PDFontFactory.java:89) at org.apache.pdfbox.pdmodel.PDResources.getFont(PDResources.java:146) at org.apache.pdfbox.contentstream.operator.text.SetFontAndSize.process(SetFontAndSize.java:66) at org.apache.pdfbox.contentstream.PDFStreamEngine.processOperator(PDFStreamEngine.java:933) at org.apache.pdfbox.contentstream.PDFStreamEngine.processStreamOperators(PDFStreamEngine.java:514) at org.apache.pdfbox.contentstream.PDFStreamEngine.processStream(PDFStreamEngine.java:492) at org.apache.pdfbox.contentstream.PDFStreamEngine.processPage(PDFStreamEngine.java:155) at org.apache.pdfbox.text.LegacyPDFStreamEngine.processPage(LegacyPDFStreamEngine.java:144) at org.apache.pdfbox.text.PDFTextStripper.processPage(PDFTextStripper.java:394) at org.apache.pdfbox.text.PDFTextStripper.processPages(PDFTextStripper.java:322) at org.apache.pdfbox.text.PDFTextStripper.writeText(PDFTextStripper.java:269) at org.apache.pdfbox.text.PDFTextStripper.getText(PDFTextStripper.java:233) at com.zzyl.common.utils.PDFUtil.pdfToString(PDFUtil.java:25) at com.zzyl.nursing.controller.HealthAssessmentController.uploadFile(HealthAssessmentController.java:134)
最新发布
08-13
<think>我们正在讨论Java IO流中的`mark()`和`reset()`方法,以及如何利用它们实现重复读取操作。根据引用内容,这两个方法主要用于在输入流中标记一个位置,之后可以通过`reset()`方法回到该位置重新读取数据。这在某些需要“回看”的场景中非常有用。 ### 核心概念 1. **`mark(int readlimit)`方法**:在输入流的当前位置设置一个标记。参数`readlimit`表示在标记失效前可以读取的最大字节数(或字符数)。如果读取的字节数超过了这个限制,标记可能会失效(但根据引用[1][4],在使用缓冲流时,即使超出限制,标记有时仍然有效)。 2. **`reset()`方法**:将流重置到最后一次调用`mark()`方法时的位置,允许重新读取从标记位置到当前重置位置之间的数据。 3. **支持情况**:不是所有的流都支持`mark()`和`reset()`。例如,`FileInputStream`默认不支持,而`BufferedInputStream`支持(引用[2])。我们可以通过`markSupported()`方法检查流是否支持标记操作。 ### 使用场景 - 需要重复读取一段数据(例如,解析不确定长度的数据时,先读取一部分进行判断,如果不符合条件则回退重新读取)。 - 替换文件中的特定字符串(引用[3]):标记当前位置,读取一段数据,检查是否包含目标字符串,若包含则替换并写入,否则重置到标记位置继续读取。 ### 实现步骤 1. **创建支持mark/reset的输入流**:通常使用缓冲流(如`BufferedInputStream`或`BufferedReader`)包装基础流。 2. **检查是否支持mark/reset**:调用`markSupported()`确认。 3. **设置标记**:在需要的位置调用`mark(readlimit)`,设置一个标记,并指定读取限制。 4. **读取数据**:读取一定量的数据(不超过`readlimit`,但缓冲流可能允许超出)。 5. **判断是否需要重置**:如果读取的数据不满足条件,调用`reset()`回到标记位置,然后尝试其他处理;如果满足条件,则继续读取。 6. **重置后重新读取**:从标记位置开始重新读取数据。 ### 示例代码:使用BufferedReader重复读取 以下示例展示如何使用`BufferedReader`的`mark()`和`reset()`方法重复读取一段文本: ```java import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class MarkResetExample { public static void main(String[] args) { try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) { // 检查是否支持mark/reset if (!reader.markSupported()) { System.out.println("mark/reset not supported"); return; } // 读取第一行并标记 String line = reader.readLine(); System.out.println("Line 1: " + line); // 设置标记,参数readlimit设为100(表示我们期望在重置前最多读取100个字符) reader.mark(100); // 读取第二行 line = reader.readLine(); System.out.println("Line 2: " + line); // 重置到标记位置(即第二行开始处) reader.reset(); // 重新读取第二行 line = reader.readLine(); System.out.println("Line 2 again: " + line); // 继续读取第三行 line = reader.readLine(); System.out.println("Line 3: " + line); } catch (IOException e) { e.printStackTrace(); } } } ``` ### 注意事项 1. **`readlimit`参数**:它表示在标记失效前可以读取的字符数(或字节数)的上限。但实际中,如果使用缓冲流,即使读取超过这个限制,标记可能仍然有效(引用[1][4])。这是因为缓冲流内部已经将数据读入缓冲区,所以重置时可以从缓冲区获取数据。但为了安全起见,应尽量保守设置该值。 2. **标记位置**:调用`mark()`时,标记的是当前读取位置。之后读取的数据超过`readlimit`后,标记可能失效(此时调用`reset()`会抛出`IOException`)。 3. **流型**:只有支持标记的流才能使用。例如: - 字节流:`BufferedInputStream`、`ByteArrayInputStream`支持。 - 字符流:`BufferedReader`、`CharArrayReader`支持。 - 而`FileInputStream`、`FileReader`等默认不支持,需要包装成缓冲流。 4. **重置次数**:只能重置到最后一次设置标记的位置,不能多次重置到不同的标记(除非再次调用`mark()`设置新标记)。 5. **资源管理**:使用`try-with-resources`确保流正确关闭。 ### 应用案例:替换文件中的字符串 引用[3]提到,可以使用`mark()`和`reset()`实现替换文件中的字符串。基本思路如下: 1. 使用`BufferedReader`读取文件,使用`BufferedWriter`写入新文件。 2. 在读取过程中,对每个可能匹配目标字符串的位置设置标记。 3. 读取一段数据(长度至少为目标字符串长度),检查是否包含目标字符串。 4. 如果包含,则重置到标记位置,然后写入替换字符串,并跳过目标字符串的长度。 5. 如果不包含,则重置到标记位置,写入当前读取的一个字符(或一小段),然后向前移动一个位置,重新设置标记。 由于实现较为复杂,且涉及边界处理,这里不展开详细代码。但核心思想就是利用`mark()`和`reset()`来回退重新读取。 ### 常见错误 - **`java.io.IOException: mark/reset not supported`**:在不支持标记的流上调用`mark()`或`reset()`(引用[2])。解决方法:使用支持标记的流(如缓冲流)包装。 - **标记失效**:读取超过`readlimit`后调用`reset()`导致标记失效。解决方法:确保读取的数据量在限制内,或使用足够大的缓冲区(如设置较大的`readlimit`)。 ### 总结 - `mark()`和`reset()`方法Java IO提供了重复读取的能力,特别适合需要“回退”的场景。 - 使用缓冲流(如`BufferedInputStream`、`BufferedReader`)可以更可靠地支持这些操作。 - 合理设置`readlimit`参数,并注意标记的有效范围。 §§相关问题§§ 1. 如何在Java中实现文件内容的字符串替换? 2. 如何判断一个IO流是否支持markreset操作? 3. 使用mark/reset时有哪些常见陷阱? 4. 除了mark/reset,还有哪些方法可以实现流的重复读取?</think>### Java IO流中`mark()``reset()`方法的使用及重复操作实现 在Java IO流中,`mark()`和`reset()`方法提供了"标记-重置"机制,允许开发者在流中设置标记点并随时回退到该位置,从而实现流的重复读取操作。以下是关键知识点和实现方法: #### 一、核心概念解析 1. **`mark(int readlimit)`方法** - 在当前流位置设置标记点 - 参数`readlimit`:指定在标记失效前最多可读取的字节数 - 示例:`inputStream.mark(1024);` // 标记当前位置,允许后续读取1024字节 2. **`reset()`方法** - 将流重置到最后一次调用`mark()`时的位置 - 示例:`inputStream.reset();` // 回退到标记位置 3. **支持检测** 使用`markSupported()`检查流是否支持标记操作: ```java if (inputStream.markSupported()) { // 支持mark/reset操作 } ``` #### 二、实现重复操作的步骤 1. **创建支持mark/reset的流** 使用缓冲流包装基础流(如`BufferedInputStream`): ```java InputStream baseStream = new FileInputStream("data.txt"); InputStream inputStream = new BufferedInputStream(baseStream); // 支持mark/reset[^2] ``` 2. **设置标记点** 在需要重复读取的起始位置设置标记: ```java inputStream.mark(1024); // 标记当前位置,允许后续读取1024字节[^1] ``` 3. **读取数据并处理** ```java byte[] buffer = new byte[256]; int bytesRead = inputStream.read(buffer); // 处理数据... ``` 4. **重置流位置** 当需要重新处理数据时回退到标记点: ```java inputStream.reset(); // 回到mark()设置的位置[^3] ``` 5. **重复读取操作** ```java bytesRead = inputStream.read(buffer); // 重新读取相同数据 ``` #### 三、完整示例:替换文件中的字符串 ```java import java.io.*; public class StringReplacer { public static void main(String[] args) throws IOException { try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("source.txt")); FileOutputStream out = new FileOutputStream("output.txt")) { if (!in.markSupported()) { throw new IOException("mark/reset not supported"); } byte[] target = "old".getBytes(); // 要替换的字符串 int matchIndex = 0; while (in.available() > 0) { in.mark(target.length); // 标记当前位置[^3] // 尝试匹配目标字符串 matchIndex = 0; while (matchIndex < target.length) { int nextByte = in.read(); if (nextByte == target[matchIndex]) { matchIndex++; } else { in.reset(); // 重置到标记位置 out.write(in.read()); // 写入不匹配的字节 break; } } // 完全匹配时执行替换 if (matchIndex == target.length) { out.write("new".getBytes()); // 写入替换内容 } } } } } ``` #### 四、关键注意事项 1. **缓冲流是必需的** - 基础流如`FileInputStream`不支持`mark/reset`,必须用`BufferedInputStream`包装[^2] - 推荐缓冲区大小 ≥ `readlimit`参数值 2. **`readlimit`参数的特殊性** - 实际可读取量可能超过`readlimit`(缓冲流会扩展能力)[^1][^4] - 但超出声明值可能导致未定义行为,应保守设置 3. **典型应用场景** - 模式匹配(如字符串替换) - 数据格式验证 - 回溯解析(如不确定长度的数据块处理) 4. **错误处理** 捕获可能的`IOException`: ```java try { inputStream.reset(); } catch (IOException e) { System.err.println("重置失败: " + e.getMessage()); } ``` #### 五、不同流型的支持情况 | 流型 | markSupported() | 说明 | |--------|----------------|------| | `FileInputStream` | ❌ | 需用`BufferedInputStream`包装 | | `BufferedInputStream` | ✅ | 最佳实践选择[^2][^4] | | `ByteArrayInputStream` | ✅ | 内存流支持 | | `CharArrayReader` | ✅ | 字符流支持[^5] | > **提示**:文本处理推荐使用`BufferedReader`+`mark()`/`reset()`,字符编码处理更安全[^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值