FilterInputStream 和 FilterOutputStream 是 Java I/O 体系中基于装饰器模式的核心抽象类,用于在不改变原有流结构的前提下动态增强功能。其常见子类如 BufferedInputStream/OutputStream(提升 I/O 性能)、DataInputStream/OutputStream(跨平台读写基本数据类型)、PrintStream(格式化输出)和 LineNumberInputStream(跟踪行号)等,分别适用于大文件处理、网络通信、日志分析、报告生成等场景。通过合理组合这些过滤流,可灵活构建高效、可维护的 I/O 操作链,同时需注意资源管理、缓冲区设置、异常处理及读写类型一致性等最佳实践。

一、核心概念与设计思想

1. 什么是FilterInputStream和FilterOutputStream?

FilterInputStream 是Java中InputStream的子类,用于"封装其他输入流,并为它们提供额外的功能"。它采用装饰器设计模式,通过组合而非继承来扩展功能。

FilterOutputStream 是OutputStream的子类,用于"封装其他输出流,并为它们提供额外的功能"。同样采用装饰器设计模式

2. 装饰器模式优势

  • 避免继承链膨胀:无需为"缓冲+加密"、"缓冲+数据类型转换"等组合创建大量子类
  • 符合开闭原则:对扩展开放,对修改关闭
  • 单一职责原则:每个类专注于单一功能(如BufferedOutputStream只负责缓冲)
  • 流的可组合性:可以将多种功能组合在一起,如BufferedInputStream + DataInputStream

二、FilterInputStream详解

1. 常用子类及功能

子类功能适用场景
BufferedInputStream为输入流提供缓冲功能,减少I/O操作次数大文件读取、频繁读取场景
DataInputStream以与机器无关方式读取基本Java数据类型跨平台数据交换、网络通信
LineNumberInputStream跟踪输入流的行号日志分析、代码解析器
PushbackInputStream提供单字节回退功能编译器词法分析

2. 详细代码示例

(1) BufferedInputStream:提高读取性能

import java.io.*;

public class BufferedInputStreamExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("large_file.txt");
             BufferedInputStream bis = new BufferedInputStream(fis, 8192)) { // 8KB缓冲区
            
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                // 处理数据
                System.out.print(new String(buffer, 0, bytesRead));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

适用场景:处理大文件时,缓冲流可以显著减少磁盘I/O次数,提升性能。尤其适合频繁读取的场景,如日志分析、大文本文件处理。

(2) DataInputStream:跨平台读取基本数据类型

import java.io.*;

public class DataInputStreamExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("binary_data.bin");
             DataInputStream dis = new DataInputStream(fis)) {
            
            int intValue = dis.readInt(); // 读取4字节整数
            double doubleValue = dis.readDouble(); // 读取8字节双精度
            boolean boolValue = dis.readBoolean(); // 读取1字节布尔
            String stringValue = dis.readUTF(); // 读取UTF-8字符串
            
            System.out.println("Read from file: " + intValue + ", " + doubleValue + ", " + boolValue + ", " + stringValue);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

适用场景:当需要在不同平台间交换结构化数据时,如网络通信、序列化存储。DataInputStream确保了数据在不同平台上的正确解释。

(3) LineNumberInputStream:跟踪行号

import java.io.*;

public class LineNumberInputStreamExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("log.txt");
             LineNumberInputStream lnis = new LineNumberInputStream(fis)) {
            
            int data;
            while ((data = lnis.read()) != -1) {
                System.out.println("Line " + lnis.getLineNumber() + ": " + (char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

适用场景:日志文件分析、代码解析器等需要跟踪读取位置的场景。可以方便地定位特定行的内容。

(4) PushbackInputStream:回退读取

import java.io.*;

public class PushbackInputStreamExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("source.txt");
             PushbackInputStream pis = new PushbackInputStream(fis, 1)) {
            
            int c;
            while ((c = pis.read()) != -1) {
                if (c == 'A') {
                    // 读取了'A',但需要回退,以便后续处理
                    pis.unread(c);
                    System.out.println("Found 'A', will process it later");
                    break;
                }
            }
            
            // 重新读取'A'
            System.out.println("After unread: " + (char) pis.read());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

适用场景:编译器词法分析、解析器实现。当需要"回看"刚刚读取的字符时非常有用。

三、FilterOutputStream详解

1. 常用子类及功能

子类功能适用场景
BufferedOutputStream为输出流提供缓冲功能,减少I/O操作次数大文件写入、频繁写入场景
DataOutputStream以与机器无关方式写入基本Java数据类型跨平台数据交换、网络通信
PrintStream提供格式化输出功能生成报告、日志记录、用户界面输出

2. 详细代码示例

(1) BufferedOutputStream:提高写入性能

import java.io.*;

public class BufferedOutputStreamExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("output.txt");
             BufferedOutputStream bos = new BufferedOutputStream(fos, 8192)) { // 8KB缓冲区
            
            String text = "This is a test string to demonstrate BufferedOutputStream. " +
                          "BufferedOutputStream reduces I/O operations by using a buffer.";
            bos.write(text.getBytes());
            
            // 显式刷新缓冲区(可选)
            bos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

适用场景:写入大文件时,缓冲流可以显著减少磁盘I/O次数,提升性能。尤其适合频繁写入的场景,如日志记录、文件生成。

(2) DataOutputStream:跨平台写入基本数据类型

import java.io.*;

public class DataOutputStreamExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("data.bin");
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             DataOutputStream dos = new DataOutputStream(bos)) {
            
            dos.writeInt(12345); // 写入4字节整数
            dos.writeDouble(3.14159); // 写入8字节双精度
            dos.writeBoolean(true); // 写入1字节布尔
            dos.writeUTF("Hello, DataOutputStream!"); // 写入UTF-8字符串
            
            System.out.println("Data written to file using DataOutputStream.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

适用场景:需要在不同平台间交换结构化数据时,如网络通信、序列化存储。DataOutputStream确保了数据在不同平台上的正确解释。

(3) PrintStream:格式化输出

import java.io.*;

public class PrintStreamExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("report.txt");
             PrintStream ps = new PrintStream(fos)) {
            
            // 使用printf格式化输出
            ps.printf("Report Generated on: %s%n", java.time.LocalDate.now());
            ps.printf("Total Records: %d%n", 1000);
            ps.printf("Average Value: %.2f%n", 45.678);
            ps.printf("Status: %s%n", "Completed");
            
            // 使用print方法输出
            ps.println("\nDetailed Report:");
            ps.println("Record 1: ID=1001, Value=45.67");
            ps.println("Record 2: ID=1002, Value=46.23");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

适用场景:生成报告、日志记录、用户界面输出等需要格式化文本的场景。PrintStream提供了方便的格式化方法,如printf。

四、组合使用示例

1. 完整的数据交换示例

import java.io.*;
import java.util.Arrays;

public class DataExchangeExample {
    public static void main(String[] args) {
        // 1. 使用DataOutputStream写入数据
        try (FileOutputStream fos = new FileOutputStream("data.bin");
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             DataOutputStream dos = new DataOutputStream(bos)) {
            
            // 写入不同类型数据
            dos.writeInt(42);
            dos.writeDouble(3.1415926);
            dos.writeBoolean(true);
            dos.writeUTF("Hello, Filter Streams!");
            
            System.out.println("Data written to file using DataOutputStream.");
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 2. 使用DataInputStream读取数据
        try (FileInputStream fis = new FileInputStream("data.bin");
             BufferedInputStream bis = new BufferedInputStream(fis);
             DataInputStream dis = new DataInputStream(bis)) {
            
            // 读取不同类型数据
            int intValue = dis.readInt();
            double doubleValue = dis.readDouble();
            boolean boolValue = dis.readBoolean();
            String stringValue = dis.readUTF();
            
            System.out.println("Data read from file:");
            System.out.println("Int: " + intValue);
            System.out.println("Double: " + doubleValue);
            System.out.println("Boolean: " + boolValue);
            System.out.println("String: " + stringValue);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2. 复杂场景:日志分析与处理

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class LogAnalysisExample {
    public static void main(String[] args) {
        // 1. 读取日志文件,分析错误行
        List<String> errorLines = new ArrayList<>();
        try (FileInputStream fis = new FileInputStream("application.log");
             BufferedInputStream bis = new BufferedInputStream(fis);
             LineNumberInputStream lnis = new LineNumberInputStream(bis);
             DataInputStream dis = new DataInputStream(lnis)) {
            
            String line;
            while ((line = dis.readUTF()) != null) {
                if (line.contains("ERROR")) {
                    errorLines.add("Line " + lnis.getLineNumber() + ": " + line);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 2. 将分析结果写入新文件
        try (FileOutputStream fos = new FileOutputStream("error_analysis.txt");
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             PrintStream ps = new PrintStream(bos)) {
            
            ps.println("Error Analysis Report");
            ps.println("=====================");
            ps.println("Total errors found: " + errorLines.size());
            ps.println();
            
            for (String errorLine : errorLines) {
                ps.println(errorLine);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

五、最佳实践与注意事项

1. 资源管理

始终使用try-with-resources,确保流被正确关闭:

// 正确做法
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    // 处理
}

// 不推荐做法(可能导致资源泄漏)
FileInputStream fis = new FileInputStream("file.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
try {
    // 处理
} finally {
    bis.close();
    fis.close();
}

2. 缓冲区大小设置

  • 默认缓冲区大小通常为8KB,可以根据实际需求调整
  • 对于大文件处理,可以增大缓冲区(如16KB、32KB)
  • 对于网络I/O,缓冲区大小应与网络传输特性匹配
BufferedInputStream bis = new BufferedInputStream(fis, 32768); // 32KB缓冲区

3. 异常处理

  • 正确捕获并处理IOException
  • 避免在finally块中抛出异常,可能导致原始异常被掩盖
try {
    // I/O操作
} catch (IOException e) {
    // 处理异常
} finally {
    // 确保流关闭
}

4. 与基本流的组合

  • 通常先创建基本流(如FileInputStream),再用装饰流包装
  • 例如:FileInputStream → BufferedInputStream → DataInputStream

5. 性能考虑

  • 不要过度装饰:仅在需要特定功能时才使用装饰流
  • 避免不必要的缓冲:对于小文件或一次性读写,可能不需要缓冲
  • 正确选择流类型:根据数据类型和应用场景选择合适的流

六、常见问题与解决方案

1. 数据类型不匹配问题

问题:使用DataInputStream读取的数据类型与DataOutputStream写入的不匹配

解决方案:确保读写使用相同的数据类型

// 写入
dos.writeInt(123);

// 读取(必须是int类型)
int value = dis.readInt(); // 正确
// int value = dis.readDouble(); // 错误!会导致数据错误

2. 缓冲区大小设置不当

问题:缓冲区太小导致频繁I/O,太大导致内存浪费

解决方案:根据实际应用场景调整缓冲区大小

// 根据文件大小和系统内存设置合适的缓冲区
int bufferSize = 8192; // 8KB,通常足够
BufferedInputStream bis = new BufferedInputStream(fis, bufferSize);

3. 跨平台数据问题

问题:在不同平台间传输二进制数据时出现问题

解决方案:使用DataInputStream/DataOutputStream进行跨平台数据交换

// 写入
dos.writeInt(12345);
dos.writeDouble(3.14159);

// 读取(在不同平台都能正确读取)
int intValue = dis.readInt();
double doubleValue = dis.readDouble();

4. 流关闭顺序问题

问题:装饰流和基本流关闭顺序错误

解决方案:使用try-with-resources自动管理关闭顺序

// 正确的嵌套结构
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    // 处理
}

七、总结

FilterInputStream和FilterOutputStream是Java I/O体系的核心设计,通过装饰器模式提供了强大的功能扩展能力。理解它们的适用场景和正确使用方法对开发高性能、可维护的Java应用至关重要。

  • BufferedInputStream/BufferedOutputStream:用于性能优化,减少I/O操作次数
  • DataInputStream/DataOutputStream:用于跨平台数据交换,确保数据正确解释
  • LineNumberInputStream:用于跟踪行号,适合日志分析
  • PrintStream:用于格式化输出,方便生成报告和日志

在实际应用中,根据具体需求选择合适的流组合,遵循最佳实践,可以显著提升应用的性能和可维护性。记住,装饰器模式的核心是组合而非继承,这使Java I/O体系具有极高的灵活性和可扩展性。