MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java I/O的字符流与字节流解析

2023-05-317.0k 阅读

Java I/O 基础概念

在 Java 编程中,输入输出(I/O)操作是非常重要的一部分,它涉及到从外部数据源读取数据以及将数据写入到外部目标。数据源和目标可以是文件、网络连接、内存缓冲区等。Java 的 I/O 类库提供了丰富的功能来处理各种 I/O 任务,其中字符流和字节流是两个重要的概念。

流的概念

流(Stream)是一个抽象的概念,表示从数据源到数据目的地的一个有序的数据序列。可以把流想象成一条管道,数据就像水流一样在管道中流动。根据数据的类型不同,流可以分为字节流和字符流。字节流主要用于处理字节数据,比如二进制文件、图像、音频等;字符流则主要用于处理字符数据,比如文本文件。

I/O 类库结构

Java 的 I/O 类库位于 java.io 包中,其结构层次分明。字节流的顶层抽象类是 InputStreamOutputStream,而字符流的顶层抽象类是 ReaderWriter。从这四个顶层抽象类派生出来了许多具体的类,以满足不同的 I/O 需求。

字节流

字节流用于处理 8 位字节数据,它是最基础的 I/O 流类型。字节流的主要类是 InputStreamOutputStream,它们是抽象类,具体的操作由它们的子类来实现。

InputStream 类

InputStream 类是所有字节输入流的超类,它定义了一些基本的读取字节数据的方法。以下是一些重要的方法:

  • int read():从输入流中读取一个字节的数据,返回值是读取的字节数据,以整数形式表示(0 - 255)。如果到达流的末尾,则返回 -1。
  • int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在字节数组 b 中。返回值是实际读取的字节数,如果到达流的末尾,则返回 -1。
  • int read(byte[] b, int off, int len):从输入流中读取最多 len 个字节的数据,并将其存储在字节数组 b 中,从偏移量 off 开始存储。返回值是实际读取的字节数,如果到达流的末尾,则返回 -1。

以下是一个使用 FileInputStreamInputStream 的子类)从文件中读取字节数据的示例:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class ByteStreamReadExample {
    public static void main(String[] args) {
        try (InputStream inputStream = new FileInputStream("example.txt")) {
            int data;
            while ((data = inputStream.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,FileInputStream 用于从名为 example.txt 的文件中读取字节数据。read() 方法每次读取一个字节,并将其转换为字符后输出。当读取到文件末尾时,read() 方法返回 -1,从而结束循环。

OutputStream 类

OutputStream 类是所有字节输出流的超类,它定义了一些基本的写入字节数据的方法。以下是一些重要的方法:

  • void write(int b):将指定的字节数据写入到输出流中。参数 b 是要写入的字节数据,只使用低 8 位。
  • void write(byte[] b):将字节数组 b 中的所有字节数据写入到输出流中。
  • void write(byte[] b, int off, int len):将字节数组 b 中从偏移量 off 开始的 len 个字节数据写入到输出流中。

以下是一个使用 FileOutputStreamOutputStream 的子类)将字节数据写入文件的示例:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class ByteStreamWriteExample {
    public static void main(String[] args) {
        String message = "Hello, Byte Stream!";
        byte[] bytes = message.getBytes();
        try (OutputStream outputStream = new FileOutputStream("output.txt")) {
            outputStream.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,首先将字符串转换为字节数组,然后使用 FileOutputStream 将字节数组中的数据写入到名为 output.txt 的文件中。

字符流

字符流用于处理 16 位 Unicode 字符数据,它是在字节流的基础上构建的,提供了更方便的字符处理功能。字符流的主要类是 ReaderWriter,它们也是抽象类,具体的操作由它们的子类来实现。

Reader 类

Reader 类是所有字符输入流的超类,它定义了一些基本的读取字符数据的方法。以下是一些重要的方法:

  • int read():从输入流中读取一个字符的数据,返回值是读取的字符数据,以整数形式表示(0 - 65535)。如果到达流的末尾,则返回 -1。
  • int read(char[] cbuf):从输入流中读取一定数量的字符,并将其存储在字符数组 cbuf 中。返回值是实际读取的字符数,如果到达流的末尾,则返回 -1。
  • int read(char[] cbuf, int off, int len):从输入流中读取最多 len 个字符的数据,并将其存储在字符数组 cbuf 中,从偏移量 off 开始存储。返回值是实际读取的字符数,如果到达流的末尾,则返回 -1。

以下是一个使用 FileReaderReader 的子类)从文件中读取字符数据的示例:

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class CharStreamReadExample {
    public static void main(String[] args) {
        try (Reader reader = new FileReader("example.txt")) {
            int data;
            while ((data = reader.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,FileReader 用于从名为 example.txt 的文件中读取字符数据。read() 方法每次读取一个字符,并将其输出。当读取到文件末尾时,read() 方法返回 -1,从而结束循环。

Writer 类

Writer 类是所有字符输出流的超类,它定义了一些基本的写入字符数据的方法。以下是一些重要的方法:

  • void write(int c):将指定的字符数据写入到输出流中。参数 c 是要写入的字符数据,只使用低 16 位。
  • void write(char[] cbuf):将字符数组 cbuf 中的所有字符数据写入到输出流中。
  • void write(char[] cbuf, int off, int len):将字符数组 cbuf 中从偏移量 off 开始的 len 个字符数据写入到输出流中。
  • void write(String str):将字符串 str 中的所有字符数据写入到输出流中。
  • void write(String str, int off, int len):将字符串 str 中从偏移量 off 开始的 len 个字符数据写入到输出流中。

以下是一个使用 FileWriterWriter 的子类)将字符数据写入文件的示例:

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class CharStreamWriteExample {
    public static void main(String[] args) {
        String message = "Hello, Char Stream!";
        try (Writer writer = new FileWriter("output.txt")) {
            writer.write(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,使用 FileWriter 将字符串 message 中的字符数据写入到名为 output.txt 的文件中。

字节流与字符流的区别

字节流和字符流虽然都是用于 I/O 操作,但它们在很多方面存在区别。

处理数据类型

字节流处理的是 8 位字节数据,适用于处理二进制数据,如图片、音频、视频等文件。而字符流处理的是 16 位 Unicode 字符数据,主要用于处理文本数据,如文本文件、字符串等。

缓冲机制

字节流本身没有内置的缓冲机制,需要手动使用 BufferedInputStreamBufferedOutputStream 来实现缓冲。而字符流的一些子类,如 BufferedReaderBufferedWriter,默认就提供了缓冲机制,这使得字符流在处理文本数据时效率更高。

编码问题

字节流不涉及字符编码问题,它只是简单地读取和写入字节数据。而字符流在处理字符数据时,需要考虑字符编码。例如,FileReaderFileWriter 默认使用系统的默认字符编码,而 InputStreamReaderOutputStreamWriter 可以指定字符编码。

字节流与字符流的转换

在实际应用中,有时需要在字节流和字符流之间进行转换。Java 提供了 InputStreamReaderOutputStreamWriter 类来实现这种转换。

InputStreamReader

InputStreamReader 是从字节流到字符流的桥梁,它将 InputStream 转换为 Reader。通过 InputStreamReader,可以指定字符编码来读取字节数据并将其转换为字符数据。

以下是一个使用 InputStreamReaderFileInputStream 转换为 Reader 并指定字符编码的示例:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;

public class ByteToCharConversionExample {
    public static void main(String[] args) {
        try (Reader reader = new InputStreamReader(new FileInputStream("example.txt"), "UTF-8")) {
            int data;
            while ((data = reader.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,InputStreamReaderFileInputStream 包装起来,并指定使用 UTF - 8 字符编码将字节数据转换为字符数据。

OutputStreamWriter

OutputStreamWriter 是从字符流到字节流的桥梁,它将 Writer 转换为 OutputStream。通过 OutputStreamWriter,可以指定字符编码来将字符数据转换为字节数据并写入到输出流中。

以下是一个使用 OutputStreamWriterFileWriter 转换为 OutputStream 并指定字符编码的示例:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

public class CharToByteConversionExample {
    public static void main(String[] args) {
        String message = "Hello, Conversion!";
        try (Writer writer = new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")) {
            writer.write(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,OutputStreamWriterFileOutputStream 包装起来,并指定使用 UTF - 8 字符编码将字符数据转换为字节数据并写入到文件中。

缓冲流

无论是字节流还是字符流,缓冲流都可以显著提高 I/O 操作的性能。

字节缓冲流

字节缓冲流包括 BufferedInputStreamBufferedOutputStreamBufferedInputStream 内部有一个缓冲区,它会一次性从底层输入流中读取多个字节到缓冲区中,当调用 read() 方法时,首先从缓冲区中读取数据,当缓冲区中的数据读完后,再从底层输入流中读取数据填充缓冲区。BufferedOutputStream 类似,它会将数据先写入缓冲区,当缓冲区满或者调用 flush() 方法时,才将缓冲区中的数据写入到底层输出流中。

以下是一个使用 BufferedInputStreamBufferedOutputStream 复制文件的示例:

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

public class ByteBufferedStreamExample {
    public static void main(String[] args) {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.txt"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("destination.txt"))) {
            int data;
            while ((data = bis.read()) != -1) {
                bos.write(data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,BufferedInputStreamsource.txt 文件中读取数据,BufferedOutputStream 将数据写入到 destination.txt 文件中。由于缓冲流的使用,减少了实际的 I/O 操作次数,提高了复制文件的效率。

字符缓冲流

字符缓冲流包括 BufferedReaderBufferedWriterBufferedReader 提供了更方便的读取字符数据的方法,如 readLine() 方法可以读取一行文本。BufferedWriter 同样提供了缓冲区,提高了写入字符数据的效率。

以下是一个使用 BufferedReaderBufferedWriter 复制文本文件的示例:

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

public class CharBufferedStreamExample {
    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("source.txt"));
             BufferedWriter bw = new BufferedWriter(new FileWriter("destination.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,BufferedReader 使用 readLine() 方法逐行读取 source.txt 文件中的文本,BufferedWriter 将读取到的每一行文本写入到 destination.txt 文件中,并使用 newLine() 方法写入换行符。

数据流

数据流是一种特殊的字节流,它提供了读取和写入基本数据类型(如 intdoubleboolean 等)以及字符串的功能。数据流类包括 DataInputStreamDataOutputStream

DataInputStream

DataInputStream 用于从输入流中读取基本数据类型和字符串。它提供了一系列的读取方法,如 readInt()readDouble()readUTF() 等。

以下是一个使用 DataInputStream 读取数据的示例:

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.txt"))) {
            int number = dis.readInt();
            double value = dis.readDouble();
            String str = dis.readUTF();
            System.out.println("Number: " + number);
            System.out.println("Value: " + value);
            System.out.println("String: " + str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,DataInputStreamdata.txt 文件中读取一个整数、一个双精度浮点数和一个字符串。文件中的数据应该是使用 DataOutputStream 按照相应格式写入的。

DataOutputStream

DataOutputStream 用于将基本数据类型和字符串写入到输出流中。它提供了一系列的写入方法,如 writeInt()writeDouble()writeUTF() 等。

以下是一个使用 DataOutputStream 写入数据的示例:

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.txt"))) {
            dos.writeInt(100);
            dos.writeDouble(3.14159);
            dos.writeUTF("Hello, Data Stream!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,DataOutputStream 将一个整数、一个双精度浮点数和一个字符串写入到 data.txt 文件中。

对象流

对象流用于将对象进行序列化和反序列化操作。序列化是将对象转换为字节序列的过程,反序列化则是将字节序列恢复为对象的过程。对象流类包括 ObjectInputStreamObjectOutputStream

ObjectOutputStream

ObjectOutputStream 用于将对象写入到输出流中。要使一个对象能够被序列化,该对象的类必须实现 Serializable 接口。

以下是一个使用 ObjectOutputStream 序列化对象的示例:

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

class Person implements java.io.Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class ObjectOutputStreamExample {
    public static void main(String[] args) {
        Person person = new Person("Alice", 25);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,Person 类实现了 Serializable 接口,ObjectOutputStreamPerson 对象写入到名为 person.ser 的文件中。

ObjectInputStream

ObjectInputStream 用于从输入流中读取对象并进行反序列化。

以下是一个使用 ObjectInputStream 反序列化对象的示例:

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("person.ser"))) {
            Person person = (Person) ois.readObject();
            System.out.println("Name: " + person.getName());
            System.out.println("Age: " + person.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,ObjectInputStreamperson.ser 文件中读取对象,并将其转换为 Person 对象,然后输出对象的属性。

随机访问文件流

随机访问文件流 RandomAccessFile 提供了对文件的随机访问功能,可以在文件的任意位置进行读写操作。它既可以像字节输入流一样读取数据,也可以像字节输出流一样写入数据。

随机访问文件流的使用

RandomAccessFile 有两个构造函数,一个接受文件名,另一个接受文件名和访问模式。访问模式有 "r"(只读)、"rw"(读写)、"rws"(读写,同步文件内容和元数据)和 "rwd"(读写,同步文件内容)。

以下是一个使用 RandomAccessFile 在文件中随机读写数据的示例:

import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileExample {
    public static void main(String[] args) {
        try (RandomAccessFile raf = new RandomAccessFile("example.txt", "rw")) {
            // 写入数据
            raf.writeUTF("Hello, Random Access!");
            raf.writeInt(42);

            // 移动文件指针到文件开头
            raf.seek(0);

            // 读取数据
            String str = raf.readUTF();
            int number = raf.readInt();
            System.out.println("String: " + str);
            System.out.println("Number: " + number);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述示例中,首先使用 RandomAccessFile 以读写模式打开 example.txt 文件,写入一个字符串和一个整数。然后通过 seek() 方法将文件指针移动到文件开头,再读取之前写入的数据。

总结

Java 的 I/O 流体系非常庞大且功能强大,字节流和字符流是其中的核心部分。字节流适用于处理二进制数据,字符流适用于处理文本数据。通过缓冲流、数据流、对象流和随机访问文件流等不同类型的流,可以满足各种复杂的 I/O 需求。在实际应用中,需要根据具体的场景选择合适的流来实现高效、准确的 I/O 操作。同时,要注意资源的正确关闭,以避免资源泄漏问题。通过深入理解和熟练运用这些 I/O 流,开发者能够更好地处理与外部数据源和目标之间的数据交互,为开发健壮、高效的 Java 应用程序奠定坚实的基础。在实际项目中,还需要考虑性能优化、异常处理等方面,以确保 I/O 操作的稳定性和可靠性。