Skip to content

IO流

IO(Input/Output)流是Java中用于处理输入和输出的机制。Java提供了丰富的IO类,用于从不同数据源读取数据或将数据写入不同目标。IO操作在Java中非常重要,常用于文件操作、网络通信、数据持久化等场景。

IO流的概念

什么是IO流?

IO流是一种数据传输的抽象,它将数据的传输看作是一种流,数据可以像水流一样从一个地方流向另一个地方。在Java中,IO流可以处理各种类型的数据,包括字节和字符。

IO流的分类

Java的IO流可以按照不同的维度进行分类:

  1. 按数据流向分类

    • 输入流:从数据源读取数据到程序中
    • 输出流:将程序中的数据写入到目标中
  2. 按数据类型分类

    • 字节流:处理字节数据,适用于所有类型的文件
    • 字符流:处理字符数据,适用于文本文件
  3. 按功能分类

    • 节点流:直接与数据源或目标相连的流
    • 处理流:在节点流的基础上提供额外功能的流

IO流的体系结构

Java的IO流体系由四个基类组成:

  • 字节输入流InputStream(抽象类)
  • 字节输出流OutputStream(抽象类)
  • 字符输入流Reader(抽象类)
  • 字符输出流Writer(抽象类)

其他所有的IO流类都是这四个基类的子类或实现。

字节流

字节流用于处理字节数据,可以处理任何类型的文件,如文本文件、图像文件、音频文件等。

InputStream(字节输入流)

InputStream是所有字节输入流的抽象基类,它定义了从数据源读取字节数据的基本方法。

InputStream的主要方法

java
// 读取一个字节,返回读取的字节值(0-255),如果到达流末尾则返回-1
int read() throws IOException;

// 读取多个字节到字节数组中,返回实际读取的字节数
int read(byte[] b) throws IOException;

// 读取指定长度的字节到字节数组的指定位置
int read(byte[] b, int off, int len) throws IOException;

// 跳过指定数量的字节
long skip(long n) throws IOException;

// 返回可以从输入流中读取(或跳过)的字节数的估计值
int available() throws IOException;

// 关闭输入流并释放相关资源
void close() throws IOException;

// 标记当前位置,之后可以通过reset方法回到该位置
void mark(int readlimit) throws IOException;

// 判断此输入流是否支持mark和reset操作
boolean markSupported();

// 重置到此输入流最近的标记位置
void reset() throws IOException;

InputStream的主要子类

  • FileInputStream:从文件中读取字节数据
  • ByteArrayInputStream:从字节数组中读取数据
  • DataInputStream:从其他输入流中读取Java基本类型数据
  • BufferedInputStream:带有缓冲区的输入流,提高读取效率

OutputStream(字节输出流)

OutputStream是所有字节输出流的抽象基类,它定义了将字节数据写入目标的基本方法。

OutputStream的主要方法

java
// 写入一个字节
void write(int b) throws IOException;

// 写入一个字节数组
void write(byte[] b) throws IOException;

// 写入字节数组的指定部分
void write(byte[] b, int off, int len) throws IOException;

// 刷新此输出流,确保所有缓冲的输出字节被写出
void flush() throws IOException;

// 关闭此输出流并释放相关资源
void close() throws IOException;

OutputStream的主要子类

  • FileOutputStream:将字节数据写入文件
  • ByteArrayOutputStream:将数据写入字节数组
  • DataOutputStream:将Java基本类型数据写入其他输出流
  • BufferedOutputStream:带有缓冲区的输出流,提高写入效率

字节流的使用示例

FileInputStream和FileOutputStream示例

java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileStreamExample {
    public static void main(String[] args) {
        String sourceFile = "source.txt";
        String targetFile = "target.txt";
        
        try (FileInputStream fis = new FileInputStream(sourceFile);
             FileOutputStream fos = new FileOutputStream(targetFile)) {
            
            int data;
            // 每次读取一个字节并写入
            while ((data = fis.read()) != -1) {
                fos.write(data);
            }
            
            System.out.println("文件复制成功!");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用缓冲区提高效率

java
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedStreamExample {
    public static void main(String[] args) {
        String sourceFile = "source.txt";
        String targetFile = "target.txt";
        
        try (FileInputStream fis = new FileInputStream(sourceFile);
             BufferedInputStream bis = new BufferedInputStream(fis);
             FileOutputStream fos = new FileOutputStream(targetFile);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            
            byte[] buffer = new byte[1024]; // 1KB的缓冲区
            int bytesRead;
            
            // 每次读取一个缓冲区的数据
            while ((bytesRead = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
            }
            
            // 刷新缓冲区,确保所有数据都被写入
            bos.flush();
            
            System.out.println("文件复制成功!");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符流

字符流用于处理字符数据,特别适用于文本文件的读写操作。字符流会自动处理字符编码问题。

Reader(字符输入流)

Reader是所有字符输入流的抽象基类,它定义了从数据源读取字符数据的基本方法。

Reader的主要方法

java
// 读取一个字符,返回读取的字符值(0-65535),如果到达流末尾则返回-1
int read() throws IOException;

// 读取多个字符到字符数组中,返回实际读取的字符数
int read(char[] cbuf) throws IOException;

// 读取指定长度的字符到字符数组的指定位置
int read(char[] cbuf, int off, int len) throws IOException;

// 跳过指定数量的字符
long skip(long n) throws IOException;

// 关闭字符输入流并释放相关资源
void close() throws IOException;

// 标记当前位置,之后可以通过reset方法回到该位置
void mark(int readAheadLimit) throws IOException;

// 判断此输入流是否支持mark和reset操作
boolean markSupported();

// 重置到此输入流最近的标记位置
void reset() throws IOException;

// 告知此流是否已准备好被读取
boolean ready() throws IOException;

Reader的主要子类

  • FileReader:从文件中读取字符数据
  • CharArrayReader:从字符数组中读取数据
  • StringReader:从字符串中读取数据
  • BufferedReader:带有缓冲区的字符输入流,提供了readLine()方法读取整行文本
  • InputStreamReader:将字节流转换为字符流,可以指定字符编码

Writer(字符输出流)

Writer是所有字符输出流的抽象基类,它定义了将字符数据写入目标的基本方法。

Writer的主要方法

java
// 写入一个字符
void write(int c) throws IOException;

// 写入一个字符数组
void write(char[] cbuf) throws IOException;

// 写入字符数组的指定部分
void write(char[] cbuf, int off, int len) throws IOException;

// 写入字符串
void write(String str) throws IOException;

// 写入字符串的指定部分
void write(String str, int off, int len) throws IOException;

// 将指定的字符序列写入此writer
Writer append(CharSequence csq) throws IOException;

// 将指定字符序列的子序列写入此writer
Writer append(CharSequence csq, int start, int end) throws IOException;

// 将指定字符追加到此writer
Writer append(char c) throws IOException;

// 刷新此输出流,确保所有缓冲的输出字符被写出
void flush() throws IOException;

// 关闭此输出流并释放相关资源
void close() throws IOException;

Writer的主要子类

  • FileWriter:将字符数据写入文件
  • CharArrayWriter:将数据写入字符数组
  • StringWriter:将数据写入字符串缓冲区
  • BufferedWriter:带有缓冲区的字符输出流,提供了newLine()方法写入换行符
  • OutputStreamWriter:将字符流转换为字节流,可以指定字符编码

字符流的使用示例

FileReader和FileWriter示例

java
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class FileReaderWriterExample {
    public static void main(String[] args) {
        String sourceFile = "text.txt";
        String targetFile = "copy.txt";
        
        try (FileReader fr = new FileReader(sourceFile);
             FileWriter fw = new FileWriter(targetFile)) {
            
            int data;
            // 每次读取一个字符并写入
            while ((data = fr.read()) != -1) {
                fw.write(data);
            }
            
            System.out.println("文本文件复制成功!");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用BufferedReader和BufferedWriter进行行处理

java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedReaderWriterExample {
    public static void main(String[] args) {
        String sourceFile = "text.txt";
        String targetFile = "copy.txt";
        
        try (BufferedReader br = new BufferedReader(new FileReader(sourceFile));
             BufferedWriter bw = new BufferedWriter(new FileWriter(targetFile))) {
            
            String line;
            // 按行读取和写入
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine(); // 写入换行符
            }
            
            System.out.println("文本文件复制成功!");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用InputStreamReader和OutputStreamWriter指定字符编码

java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class EncodingExample {
    public static void main(String[] args) {
        String sourceFile = "utf8.txt";
        String targetFile = "gbk.txt";
        
        try (// 使用UTF-8编码读取文件
             BufferedReader br = new BufferedReader(new InputStreamReader(
                     new FileInputStream(sourceFile), "UTF-8"));
             // 使用GBK编码写入文件
             BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
                     new FileOutputStream(targetFile), "GBK"))) {
            
            String line;
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine();
            }
            
            System.out.println("编码转换成功!");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

转换流

转换流是连接字节流和字符流的桥梁,可以将字节流转换为字符流,或者将字符流转换为字节流。

InputStreamReader

InputStreamReader是字节流到字符流的桥梁,它读取字节并将其解码为字符。它使用指定的字符编码或默认编码将字节转换为字符。

java
// 创建一个使用默认字符编码的InputStreamReader
InputStreamReader isr = new InputStreamReader(new FileInputStream("file.txt"));

// 创建一个使用指定字符编码的InputStreamReader
InputStreamReader isrUtf8 = new InputStreamReader(
        new FileInputStream("file.txt"), "UTF-8");

OutputStreamWriter

OutputStreamWriter是字符流到字节流的桥梁,它将字符编码为字节。它使用指定的字符编码或默认编码将字符转换为字节。

java
// 创建一个使用默认字符编码的OutputStreamWriter
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("file.txt"));

// 创建一个使用指定字符编码的OutputStreamWriter
OutputStreamWriter oswUtf8 = new OutputStreamWriter(
        new FileOutputStream("file.txt"), "UTF-8");

缓冲流

缓冲流是一种处理流,它通过缓冲区提高IO操作的效率。缓冲流可以减少实际读写硬盘的次数,从而提高程序的运行效率。

BufferedInputStream

BufferedInputStream是带有缓冲区的字节输入流,它可以提高读取字节的效率。

java
// 创建一个使用默认缓冲区大小的BufferedInputStream
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("file.txt"));

// 创建一个使用指定缓冲区大小的BufferedInputStream
BufferedInputStream bisCustom = new BufferedInputStream(
        new FileInputStream("file.txt"), 8192); // 8KB缓冲区

BufferedOutputStream

BufferedOutputStream是带有缓冲区的字节输出流,它可以提高写入字节的效率。

java
// 创建一个使用默认缓冲区大小的BufferedOutputStream
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("file.txt"));

// 创建一个使用指定缓冲区大小的BufferedOutputStream
BufferedOutputStream bosCustom = new BufferedOutputStream(
        new FileOutputStream("file.txt"), 8192); // 8KB缓冲区

BufferedReader

BufferedReader是带有缓冲区的字符输入流,它提供了readLine()方法来读取整行文本。

java
// 创建一个使用默认缓冲区大小的BufferedReader
BufferedReader br = new BufferedReader(new FileReader("file.txt"));

// 创建一个使用指定缓冲区大小的BufferedReader
BufferedReader brCustom = new BufferedReader(
        new FileReader("file.txt"), 8192); // 8KB缓冲区

BufferedWriter

BufferedWriter是带有缓冲区的字符输出流,它提供了newLine()方法来写入平台特定的换行符。

java
// 创建一个使用默认缓冲区大小的BufferedWriter
BufferedWriter bw = new BufferedWriter(new FileWriter("file.txt"));

// 创建一个使用指定缓冲区大小的BufferedWriter
BufferedWriter bwCustom = new BufferedWriter(
        new FileWriter("file.txt"), 8192); // 8KB缓冲区

数据流

数据流用于读写Java基本数据类型。数据流可以将Java的基本数据类型(如int、float、double等)写入到输出流中,或者从输入流中读取Java的基本数据类型。

DataInputStream

DataInputStream可以从其他输入流中读取Java基本类型数据。

java
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class DataInputStreamExample {
    public static void main(String[] args) {
        try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
            // 按照写入的顺序读取数据
            int intValue = dis.readInt();
            double doubleValue = dis.readDouble();
            boolean booleanValue = dis.readBoolean();
            String stringValue = dis.readUTF();
            
            System.out.println("int: " + intValue);
            System.out.println("double: " + doubleValue);
            System.out.println("boolean: " + booleanValue);
            System.out.println("string: " + stringValue);
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

DataOutputStream

DataOutputStream可以将Java基本类型数据写入到其他输出流中。

java
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataOutputStreamExample {
    public static void main(String[] args) {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
            // 写入不同类型的数据
            dos.writeInt(42);
            dos.writeDouble(3.14159);
            dos.writeBoolean(true);
            dos.writeUTF("Hello, Data Streams!");
            
            System.out.println("数据写入成功!");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

对象流

对象流用于读写Java对象。使用对象流可以将Java对象序列化后写入到输出流中,或者从输入流中读取序列化的数据并反序列化为Java对象。

ObjectInputStream

ObjectInputStream用于从输入流中读取序列化的对象。

java
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectInputStreamExample {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.bin"))) {
            // 读取对象
            Person person = (Person) ois.readObject();
            
            System.out.println("读取到的对象: " + person);
            
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

ObjectOutputStream

ObjectOutputStream用于将对象序列化后写入到输出流中。

java
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

// 要序列化的类必须实现Serializable接口
class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class ObjectOutputStreamExample {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.bin"))) {
            // 写入对象
            oos.writeObject(person);
            
            System.out.println("对象写入成功!");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

序列化的注意事项

  1. 要序列化的类必须实现Serializable接口
  2. 可以使用serialVersionUID来控制版本兼容性
  3. transient关键字修饰的字段不会被序列化
  4. 静态字段不会被序列化
  5. 如果一个类的父类实现了Serializable接口,那么该类默认也可以序列化

NIO

Java NIO(New IO)是Java 1.4引入的一组新的IO API,它提供了更高效的IO操作方式。NIO与传统IO的主要区别在于:

  • NIO使用基于缓冲区(Buffer)的方式进行IO操作
  • NIO提供了非阻塞IO操作
  • NIO引入了通道(Channel)的概念
  • NIO提供了选择器(Selector)用于多路复用

NIO的核心组件

  1. Buffer:数据容器,用于存储数据
  2. Channel:数据传输的通道
  3. Selector:用于监听多个通道的事件
  4. Charset:用于字符编码和解码
  5. Path:表示文件系统中的路径
  6. Files:提供文件操作的工具方法

NIO 2

Java 7引入了NIO 2(也称JSR 203),它扩展了NIO,提供了更强大的文件操作功能:

  • Path:表示文件系统路径
  • Files:提供了丰富的文件操作方法
  • FileVisitor:用于遍历文件树
  • WatchService:用于监听文件系统变化

NIO 2的使用示例

java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;

public class NIO2Example {
    public static void main(String[] args) {
        try {
            // 创建文件
            Path newFile = Paths.get("newFile.txt");
            Files.createFile(newFile);
            System.out.println("文件创建成功: " + newFile);
            
            // 写入文件
            String content = "Hello, NIO 2!\nThis is a test file.";
            Files.write(newFile, content.getBytes());
            System.out.println("文件写入成功");
            
            // 读取文件
            List<String> lines = Files.readAllLines(newFile);
            System.out.println("文件内容:");
            for (String line : lines) {
                System.out.println(line);
            }
            
            // 复制文件
            Path copyFile = Paths.get("copyFile.txt");
            Files.copy(newFile, copyFile, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("文件复制成功: " + copyFile);
            
            // 移动文件
            Path moveFile = Paths.get("movedFile.txt");
            Files.move(copyFile, moveFile, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("文件移动成功: " + moveFile);
            
            // 删除文件
            Files.delete(newFile);
            Files.delete(moveFile);
            System.out.println("文件删除成功");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

IO流的最佳实践

  1. 使用try-with-resources语句

    • 自动关闭资源,避免资源泄露
    • 代码更加简洁、清晰
  2. 选择合适的IO流

    • 处理文本文件时使用字符流
    • 处理二进制文件时使用字节流
    • 需要高效读写时使用缓冲流
  3. 注意字符编码问题

    • 明确指定字符编码,避免平台相关的编码问题
    • 使用UTF-8作为默认编码是一个好习惯
  4. 使用缓冲区提高效率

    • 使用缓冲流可以减少实际IO操作的次数
    • 合理设置缓冲区大小
  5. 及时关闭资源

    • 即使使用try-with-resources,也应该确保资源被正确关闭
    • 输出流在关闭前应该调用flush()方法确保数据被完全写入
  6. 异常处理

    • 捕获并适当处理IO异常
    • 提供有意义的错误信息

常见IO操作示例

文件复制

java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

public class FileCopyExample {
    public static void main(String[] args) {
        String source = "source.txt";
        String target = "target.txt";
        
        try {
            // 使用NIO 2的Files.copy方法复制文件
            Files.copy(Paths.get(source), Paths.get(target), StandardCopyOption.REPLACE_EXISTING);
            System.out.println("文件复制成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文件读取

java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class FileReadExample {
    public static void main(String[] args) {
        String filePath = "example.txt";
        
        try {
            // 读取所有行
            List<String> lines = Files.readAllLines(Paths.get(filePath));
            
            // 打印文件内容
            System.out.println("文件内容:");
            for (String line : lines) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文件夹遍历

java
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;

public class DirectoryWalkExample {
    public static void main(String[] args) {
        String directory = "./src";
        
        try {
            Files.walkFileTree(Paths.get(directory), EnumSet.of(FileVisitOption.FOLLOW_LINKS), 
                    Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
                
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    System.out.println("文件: " + file);
                    return FileVisitResult.CONTINUE;
                }
                
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    System.out.println("目录: " + dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

小结

  • Java IO流是用于处理输入输出的机制,分为字节流和字符流两大类
  • 字节流以字节为单位进行操作,适用于所有类型的文件
  • 字符流以字符为单位进行操作,适用于文本文件,自动处理字符编码
  • 缓冲流可以提高IO操作的效率
  • 转换流用于连接字节流和字符流
  • 数据流用于读写Java基本数据类型
  • 对象流用于读写Java对象,涉及序列化和反序列化
  • NIO提供了更高效的IO操作方式,引入了缓冲区、通道、选择器等概念
  • NIO 2进一步扩展了NIO,提供了更强大的文件操作功能
  • 使用try-with-resources语句可以自动关闭资源,避免资源泄露

掌握Java IO是Java编程的重要基础,合理地使用各种IO流可以使程序更加高效、可靠。