Java学习十—IO

一、关于流

1.1简介

Java I/O 流是Java程序中处理输入和输出的基础。在Java中,I/O 流被广泛用于读取和写入数据,这些数据可以来自文件、网络连接、内存缓冲区等。Java I/O 流是建立在字节流和字符流的基础上的,它们通过不同的类和接口提供了丰富的数据处理功能。

Java学习十—IO
image-20230523011308466

1.2特性

  • • 先进先出:最先写入输出流的数据最先被输入流读取到。

  • • 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)

  • • 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。

1.3分类

1.3.1流向划分

Java学习十—IO
image1(1)

1.3.2处理数据单元划分(字节与字符)

Java学习十—IO
image2(1)

Java学习十—IO
image

传输方式有两种,字节和字符,那首先得搞明白字节和字符有什么区别,对吧?

字节(byte)是计算机中用来表示存储容量的一个计量单位,通常情况下,一个字节有 8 位(bit)。

字符(char)可以是计算机中使用的字母、数字、和符号,比如说 A 1 $ 这些。

通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节。

Java学习十—IO

具体还要看字符编码,比如说在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节。

明白了字节与字符的区别,再来看字节流和字符流就会轻松多了。

字节流用来处理二进制文件,比如说图片啊、MP3 啊、视频啊。

字符流用来处理文本文件,文本文件可以看作是一种特殊的二进制文件,只不过经过了编码,便于人们阅读。

换句话说就是,字节流可以处理一切文件,而字符流只能处理文本。

虽然 IO 类很多,但核心的就是 4 个抽象类:InputStream、OutputStream、Reader、Writer。

抽象大法真好

虽然 IO 类的方法也很多,但核心的也就 2 个:read 和 write。

InputStream 类

  • • int read():读取数据

  • • int read(byte b[], int off, int len):从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中

  • • long skip(long n):跳过指定个数的字节

  • • int available():返回可读的字节数

  • • void close():关闭流,释放资源

OutputStream 类

  • • void write(int b): 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲)

  • • void write(byte b[], int off, int len): 将数组 b 中的从 off 位置开始,长度为 len 的字节写入

  • • void flush(): 强制刷新,将缓冲区的数据写入

  • • void close():关闭流

Reader 类

  • • int read():读取单个字符

  • • int read(char cbuf[], int off, int len):从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中

  • • long skip(long n):跳过指定个数的字符

  • • int ready():是否可以读了

  • • void close():关闭流,释放资源

Writer 类

  • • void write(int c): 写入一个字符

  • • void write( char cbuf[], int off, int len): 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入

  • • void flush(): 强制刷新,将缓冲区的数据写入

  • • void close():关闭流

理解了上面这些方法,基本上 IO 的灵魂也就全部掌握了。

字节流和字符流的区别:

  • • 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。

  • • 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。

以写文件为例,我们查看字符流的源码,发现确实有利用到缓冲区:

// 声明一个 char 类型的数组,用于写入输出流
private char[] writeBuffer;

// 定义 writeBuffer 数组的大小,必须 >= 1
private static final int WRITE_BUFFER_SIZE = 1024;

// 写入给定字符串中的一部分到输出流中
public void write(String str, int off, int len) throws IOException {
    // 使用 synchronized 关键字同步代码块,确保线程安全
    synchronized (lock) {
        char cbuf[];
        // 如果 len <= WRITE_BUFFER_SIZE,则使用 writeBuffer 数组进行写入
        if (len <= WRITE_BUFFER_SIZE) {
            // 如果 writeBuffer 为 null,则创建一个大小为 WRITE_BUFFER_SIZE 的新 char 数组
            if (writeBuffer == null) {
                writeBuffer = new char[WRITE_BUFFER_SIZE];
            }
            cbuf = writeBuffer;
        } else {    // 如果 len > WRITE_BUFFER_SIZE,则不永久分配非常大的缓冲区
            // 创建一个大小为 len 的新 char 数组
            cbuf = new char[len];
        }
        // 将 str 中的一部分(从 off 开始,长度为 len)拷贝到 cbuf 数组中
        str.getChars(off, (off + len), cbuf, 0);
        // 将 cbuf 数组中的数据写入输出流中
        write(cbuf, 0, len);
    }
}

这段代码是 Java IO 类库中的 OutputStreamWriter 类的 write 方法,可以看到缓冲区的大小是 1024 个 char。

我们再以文件的字符流和字节流来做一下对比,代码差别很小。

// 字节流
try (FileInputStream fis = new FileInputStream("input.txt");
     FileOutputStream fos = new FileOutputStream("output.txt")) {
    byte[] buffer = new byte[1024];
    int len;
    while ((len = fis.read(buffer)) != -1) {
        fos.write(buffer, 0, len);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 字符流
try (FileReader fr = new FileReader("input.txt");
     FileWriter fw = new FileWriter("output.txt")) {
    char[] buffer = new char[1024];
    int len;
    while ((len = fr.read(buffer)) != -1) {
        fw.write(buffer, 0, len);
    }
} catch (IOException e) {
    e.printStackTrace();
}

1.3.3操作对象划分

IO,不就是输入输出(Input/Output)嘛:

  • • Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等

  • • Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。

所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。

文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。

Java学习十—IO

1)文件

文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。

FileInputStream 的例子:

// 声明一个 int 类型的变量 b,用于存储读取到的字节
int b;
// 创建一个 FileInputStream 对象,用于读取文件 fis.txt 中的数据
FileInputStream fis1 = new FileInputStream("fis.txt");

// 循环读取文件中的数据
while ((= fis1.read()) != -1) {
    // 将读取到的字节转换为对应的 ASCII 字符,并输出到控制台
    System.out.println((char)b);
}

// 关闭 FileInputStream 对象,释放资源
fis1.close();

FileOutputStream 的例子:

// 创建一个 FileOutputStream 对象,用于写入数据到文件 fos.txt 中
FileOutputStream fos = new FileOutputStream("fos.txt");

// 向文件中写入数据,这里写入的是字符串 "沉默王二" 对应的字节数组
fos.write("沉默王二".getBytes());

// 关闭 FileOutputStream 对象,释放资源
fos.close();

FileReader 的例子:

// 声明一个 int 类型的变量 b,用于存储读取到的字符
int b = 0;

// 创建一个 FileReader 对象,用于读取文件 read.txt 中的数据
FileReader fileReader = new FileReader("read.txt");

// 循环读取文件中的数据
while ((= fileReader.read()) != -1) {
    // 将读取到的字符强制转换为 char 类型,并输出到控制台
    System.out.println((char)b);
}

// 关闭 FileReader 对象,释放资源
fileReader.close();

FileWriter 的例子:

// 创建一个 FileWriter 对象,用于写入数据到文件 fw.txt 中
FileWriter fileWriter = new FileWriter("fw.txt");

// 将字符串 "沉默王二" 转换为字符数组
char[] chars = "沉默王二".toCharArray();

// 向文件中写入数据,这里写入的是 chars 数组中的所有字符
fileWriter.write(chars, 0, chars.length);

// 关闭 FileWriter 对象,释放资源
fileWriter.close();

文件流还可以用于创建、删除、重命名文件等操作。FileOutputStream 和 FileWriter 构造函数的第二个参数可以指定是否追加数据到文件末尾。

示例代码:

// 创建文件
File file = new File("test.txt");
if (file.createNewFile()) {
    System.out.println("文件创建成功");
} else {
    System.out.println("文件已存在");
}

// 删除文件
if (file.delete()) {
    System.out.println("文件删除成功");
} else {
    System.out.println("文件删除失败");
}

// 重命名文件
File oldFile = new File("old.txt");
File newFile = new File("new.txt");
if (oldFile.renameTo(newFile)) {
    System.out.println("文件重命名成功");
} else {
    System.out.println("文件重命名失败");
}

当掌握了文件的输入输出,其他的自然也就掌握了,都大差不差。

2)数组(内存)

通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流。

ByteArrayInputStream 的例子:

// 创建一个 ByteArrayInputStream 对象,用于从字节数组中读取数据
InputStream is = new BufferedInputStream(
        new ByteArrayInputStream(
                "沉默王二".getBytes(StandardCharsets.UTF_8)));

// 定义一个字节数组用于存储读取到的数据
byte[] flush = new byte[1024];

// 定义一个变量用于存储每次读取到的字节数
int len = 0;

// 循环读取字节数组中的数据,并输出到控制台
while (-1 != (len = is.read(flush))) {
    // 将读取到的字节转换为对应的字符串,并输出到控制台
    System.out.println(new String(flush, 0, len));
}

// 关闭输入流,释放资源
is.close();

ByteArrayOutputStream 的例子:

// 创建一个 ByteArrayOutputStream 对象,用于写入数据到内存缓冲区中
ByteArrayOutputStream bos = new ByteArrayOutputStream();

// 定义一个字节数组用于存储要写入内存缓冲区中的数据
byte[] info = "沉默王二".getBytes();

// 向内存缓冲区中写入数据,这里写入的是 info 数组中的所有字节
bos.write(info, 0, info.length);

// 将内存缓冲区中的数据转换为字节数组
byte[] dest = bos.toByteArray();

// 关闭 ByteArrayOutputStream 对象,释放资源
bos.close();

数组流可以用于在内存中读写数据,比如将数据存储在字节数组中进行压缩、加密、序列化等操作。它的优点是不需要创建临时文件,可以提高程序的效率。但是,数组流也有缺点,它只能存储有限的数据量,如果存储的数据量过大,会导致内存溢出。

3)管道

Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。

一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。

// 创建一个 PipedOutputStream 对象和一个 PipedInputStream 对象
final PipedOutputStream pipedOutputStream = new PipedOutputStream();
final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);

// 创建一个线程,向 PipedOutputStream 中写入数据
Thread thread1 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            // 将字符串 "沉默王二" 转换为字节数组,并写入到 PipedOutputStream 中
            pipedOutputStream.write("沉默王二".getBytes(StandardCharsets.UTF_8));
            // 关闭 PipedOutputStream,释放资源
            pipedOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

// 创建一个线程,从 PipedInputStream 中读取数据并输出到控制台
Thread thread2 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            // 定义一个字节数组用于存储读取到的数据
            byte[] flush = new byte[1024];
            // 定义一个变量用于存储每次读取到的字节数
            int len = 0;
            // 循环读取字节数组中的数据,并输出到控制台
            while (-1 != (len = pipedInputStream.read(flush))) {
                // 将读取到的字节转换为对应的字符串,并输出到控制台
                System.out.println(new String(flush, 0, len));
            }
            // 关闭 PipedInputStream,释放资源
            pipedInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

// 启动线程1和线程2
thread1.start();
thread2.start();

使用管道流可以实现不同线程之间的数据传输,可以用于线程间的通信、数据的传递等。但是,管道流也有一些局限性,比如只能在同一个 JVM 中的线程之间使用,不能跨越不同的 JVM 进程。

4)基本数据类型

基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。

DataInputStream 提供了一系列可以读基本数据类型的方法:

// 创建一个 DataInputStream 对象,用于从文件中读取数据
DataInputStream dis = new DataInputStream(new FileInputStream("das.txt"));

// 读取一个字节,将其转换为 byte 类型
byte b = dis.readByte();

// 读取两个字节,将其转换为 short 类型
short s = dis.readShort();

// 读取四个字节,将其转换为 int 类型
int i = dis.readInt();

// 读取八个字节,将其转换为 long 类型
long l = dis.readLong();

// 读取四个字节,将其转换为 float 类型
float f = dis.readFloat();

// 读取八个字节,将其转换为 double 类型
double d = dis.readDouble();

// 读取一个字节,将其转换为 boolean 类型
boolean bb = dis.readBoolean();

// 读取两个字节,将其转换为 char 类型
char ch = dis.readChar();

// 关闭 DataInputStream,释放资源
dis.close();

DataOutputStream 提供了一系列可以写基本数据类型的方法:

// 创建一个 DataOutputStream 对象,用于将数据写入到文件中
DataOutputStream das = new DataOutputStream(new FileOutputStream("das.txt"));

// 将一个 byte 类型的数据写入到文件中
das.writeByte(10);

// 将一个 short 类型的数据写入到文件中
das.writeShort(100);

// 将一个 int 类型的数据写入到文件中
das.writeInt(1000);

// 将一个 long 类型的数据写入到文件中
das.writeLong(10000L);

// 将一个 float 类型的数据写入到文件中
das.writeFloat(12.34F);

// 将一个 double 类型的数据写入到文件中
das.writeDouble(12.56);

// 将一个 boolean 类型的数据写入到文件中
das.writeBoolean(true);

// 将一个 char 类型的数据写入到文件中
das.writeChar('A');

// 关闭 DataOutputStream,释放资源
das.close();

除了 DataInputStream 和 DataOutputStream,Java IO 还提供了其他一些读写基本数据类型和字符串的流类,包括 ObjectInputStream 和 ObjectOutputStream(用于读写对象)。

示例代码:

public static void main(String[] args) {
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"))) {
        Person p = new Person("张三", 20);
        oos.writeObject(p);
    } catch (IOException e) {
        e.printStackTrace();
    }

    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"))) {
        Person p = (Person) ois.readObject();
        System.out.println(p);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

以上代码创建了一个 Person 对象,将其写入文件中,然后从文件中读取该对象,并打印在控制台上。

5)缓冲

CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。

为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。

Java学习十—IO

缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。

以下是一个使用 BufferedInputStream 读取文件的示例代码:

// 创建一个 BufferedInputStream 对象,用于从文件中读取数据
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("data.txt"));

// 创建一个字节数组,作为缓存区
byte[] buffer = new byte[1024];

// 读取文件中的数据,并将其存储到缓存区中
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
    // 对缓存区中的数据进行处理
    // 这里只是简单地将读取到的字节数组转换为字符串并打印出来
    System.out.println(new String(buffer, 0, bytesRead));
}

// 关闭 BufferedInputStream,释放资源
bis.close();

上述代码中,首先创建了一个 BufferedInputStream 对象,用于从文件中读取数据。然后创建了一个字节数组作为缓存区,每次读取数据时将数据存储到缓存区中。读取数据的过程是通过 while 循环实现的,每次读取数据后对缓存区中的数据进行处理。最后关闭 BufferedInputStream,释放资源。

以下是一个使用 BufferedOutputStream 写入文件的示例代码:

// 创建一个 BufferedOutputStream 对象,用于将数据写入到文件中
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("data.txt"));

// 创建一个字节数组,作为缓存区
byte[] buffer = new byte[1024];

// 将数据写入到文件中
String data = "沉默王二是个大傻子!";
buffer = data.getBytes();
bos.write(buffer);

// 刷新缓存区,将缓存区中的数据写入到文件中
bos.flush();

// 关闭 BufferedOutputStream,释放资源
bos.close();

上述代码中,首先创建了一个 BufferedOutputStream 对象,用于将数据写入到文件中。然后创建了一个字节数组作为缓存区,将数据写入到缓存区中。写入数据的过程是通过 write() 方法实现的,将字节数组作为参数传递给 write() 方法即可。

最后,通过 flush() 方法将缓存区中的数据写入到文件中。在写入数据时,由于使用了 BufferedOutputStream,数据会先被写入到缓存区中,只有在缓存区被填满或者调用了 flush() 方法时才会将缓存区中的数据写入到文件中。

以下是一个使用 BufferedReader 读取文件的示例代码:

// 创建一个 BufferedReader 对象,用于从文件中读取数据
BufferedReader br = new BufferedReader(new FileReader("data.txt"));

// 读取文件中的数据,并将其存储到字符串中
String line;
while ((line = br.readLine()) != null) {
    // 对读取到的数据进行处理
    // 这里只是简单地将读取到的每一行字符串打印出来
    System.out.println(line);
}

// 关闭 BufferedReader,释放资源
br.close();

上述代码中,首先创建了一个 BufferedReader 对象,用于从文件中读取数据。然后使用 readLine() 方法读取文件中的数据,每次读取一行数据并将其存储到一个字符串中。读取数据的过程是通过 while 循环实现的。

以下是一个使用 BufferedWriter 写入文件的示例代码:

// 创建一个 BufferedWriter 对象,用于将数据写入到文件中
BufferedWriter bw = new BufferedWriter(new FileWriter("data.txt"));

// 将数据写入到文件中
String data = "沉默王二,真帅气";
bw.write(data);

// 刷新缓存区,将缓存区中的数据写入到文件中
bw.flush();

// 关闭 BufferedWriter,释放资源
bw.close();

上述代码中,首先创建了一个 BufferedWriter 对象,用于将数据写入到文件中。然后使用 write() 方法将数据写入到缓存区中,写入数据的过程和使用 FileWriter 类似。需要注意的是,使用 BufferedWriter 写入数据时,数据会先被写入到缓存区中,只有在缓存区被填满或者调用了 flush() 方法时才会将缓存区中的数据写入到文件中。

最后,通过 flush() 方法将缓存区中的数据写入到文件中,并通过 close() 方法关闭 BufferedWriter,释放资源。

使用缓冲流可以提高读写效率,减少了频繁的读写磁盘或网络的次数,从而提高了程序的性能。但是,在使用缓冲流时需要注意缓冲区的大小和清空缓冲区的时机,以避免数据丢失或不完整的问题。

6)打印

Java 的打印流是一组用于打印输出数据的类,包括 PrintStream 和 PrintWriter 两个类。

恐怕 Java 程序员一生当中最常用的就是打印流了:System.out 其实返回的就是一个 PrintStream 对象,可以用来打印各式各样的对象。

System.out.println("沉默王二是真的二!");

PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的 print()/println() 方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样。

StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
    pw.println("沉默王二");
}
System.out.println(buffer.toString());

7)对象序列化/反序列化

序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。

// 创建一个 ByteArrayOutputStream 对象 buffer,用于存储数据
ByteArrayOutputStream buffer = new ByteArrayOutputStream();

// 使用 try-with-resources 语句创建一个 ObjectOutputStream 对象 output,并将其与 buffer 关联
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
  
    // 使用 writeUTF() 方法将字符串 "沉默王二" 写入到缓冲区中
    output.writeUTF("沉默王二");
}

// 使用 toByteArray() 方法将缓冲区中的数据转换成字节数组,并输出到控制台
System.out.println(Arrays.toString(buffer.toByteArray()));

与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。

try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(
        new File("Person.txt")))) {
    String s = input.readUTF();
}

这段代码主要使用了 Java 的 ByteArrayOutputStream 和 ObjectOutputStream 类,将字符串 “沉默王二” 写入到一个字节数组缓冲区中,并将缓冲区中的数据转换成字节数组输出到控制台。

具体的执行过程如下:

  • • 创建一个 ByteArrayOutputStream 对象 buffer,用于存储数据。

  • • 使用 try-with-resources 语句创建一个 ObjectOutputStream 对象 output,并将其与 buffer 关联。

  • • 使用 writeUTF() 方法将字符串 “沉默王二” 写入到缓冲区中。

  • • 当 try-with-resources 语句执行完毕时,会自动调用 output 的 close() 方法关闭输出流,释放资源。

  • • 使用 toByteArray() 方法将缓冲区中的数据转换成字节数组。

  • • 使用 Arrays.toString() 方法将字节数组转换成字符串,并输出到控制台。

8)转换

InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。

// 创建一个 InputStreamReader 对象 isr,使用 FileInputStream 对象读取文件 demo.txt 的内容并将其转换为字符流
InputStreamReader isr = new InputStreamReader(new FileInputStream("demo.txt"));

// 创建一个字符数组 cha,用于存储读取的字符数据,其中 1024 表示数组的长度
char[] cha = new char[1024];

// 使用 read() 方法读取 isr 中的数据,并将读取的字符数据存储到 cha 数组中,返回值 len 表示读取的字符数
int len = isr.read(cha);

// 将 cha 数组中从下标 0 开始、长度为 len 的部分转换成字符串,并输出到控制台
System.out.println(new String(cha, 0, len));

// 关闭 InputStreamReader 对象 isr,释放资源
isr.close();

这段代码主要使用了 Java 的 InputStreamReader 和 FileInputStream 类,从文件 demo.txt 中读取数据并将其转换为字符流,然后将读取的字符数据存储到一个字符数组中,并输出转换成字符串后的结果到控制台。

OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。

// 创建一个 File 对象 f,表示文件 test.txt
File f = new File("test.txt");

// 创建一个 OutputStreamWriter 对象 out,使用 FileOutputStream 对象将数据写入到文件 f 中,并将字节流转换成字符流
Writer out = new OutputStreamWriter(new FileOutputStream(f));

// 使用 write() 方法将字符串 "沉默王二!!" 写入到文件 f 中
out.write("沉默王二!!");

// 关闭 Writer 对象 out,释放资源
out.close();

使用转换流可以方便地在字节流和字符流之间进行转换。在进行文本文件读写时,通常使用字符流进行操作,而在进行网络传输或与设备进行通信时,通常使用字节流进行操作。

另外,在使用转换流时需要注意字符编码的问题。如果不指定字符编码,则使用默认的字符编码,可能会出现乱码问题。因此,建议在使用转换流时,始终指定正确的字符编码,以避免出现乱码问题。

“小二啊,你看,经过我的梳理,是不是感觉 IO 也没多少东西!针对不同的场景、不同的业务,选择对应的 IO 流就可以了,用法上就是读和写。”老王一口气讲完这些,长长的舒了一口气。

此时此刻的小二,还沉浸在老王的滔滔不绝中。不仅感觉老王的肺活量是真的大,还感慨老王不愧是工作了十多年的“老油条”,一下子就把自己感觉头大的 IO 给梳理得很清晰了。

二、文件流

2.1关于文件流

Java 文件流是 Java I/O API 中用于读写文件的一种方式。它们允许你从文件中读取数据或将数据写入文件。文件流通常用于处理字节数据,但也可以用于处理字符数据。

Java学习十—IO
image-20230523011059089

在 IO 操作中,文件的操作相对来说是比较复杂的,但也是使用频率最高的部分,我们几乎所有的项目中几乎都躺着一个叫做 FileUtil 或者 FileUtils 的工具类。

2.2File类

2.2.1简介

java.io.File 类是专门对文件进行操作的类,注意只能对文件本身进行操作,不能对文件内容进行操作,想要操作内容,必须借助输入输出流。

File 类是文件和目录的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

怎么理解上面两句话?其实很简单!

第一句是说 File 跟流无关,File 类不能对文件进行读和写,也就是输入和输出!

第二句是说 File 可以表示D:\文件目录1D:\文件目录1\文件.txt,前者是文件夹(Directory,或者叫目录)后者是文件(file),File 类就是用来操作它俩的。

Java学习十—IO
image

2.2.2构造方法

在 Java 中,一切皆是对象,File 类也不例外,不论是哪个对象都应该从该对象的构造说起,所以我们来分析分析File类的构造方法。

比较常用的构造方法有三个:

1、 File(String pathname) :通过给定的路径来创建新的 File 实例。

2、 File(String parent, String child) :从父路径(字符串)和子路径创建新的 File 实例。

3、 File(File parent, String child) :从父路径(File)和子路径名字符串创建新的 File 实例。

看文字描述不够生动、不够形象、不得劲?没事,通过举例马上就生动形象了,代码如下:

// 文件路径名
String path = "/Users/username/123.txt";
File file1 = new File(path);
// 文件路径名
String path2 = "/Users/username/1/2.txt";
File file2 = new File(path2); -------------相当于/Users/username/1/2.txt
// 通过父路径和子路径字符串
String parent = "/Users/username/aaa";
String child = "bbb.txt";
File file3 = new File(parent, child); --------相当于/Users/username/aaa/bbb.txt
// 通过父级File对象和子路径字符串
File parentDir = new File("/Users/username/aaa");
String child = "bbb.txt";
File file4 = new File(parentDir, child); --------相当于/Users/username/aaa/bbb.txt

注意,macOS 路径使用正斜杠(/)作为路径分隔符,而 Windows 路径使用反斜杠()作为路径分隔符。所以在遇到路径分隔符的时候,不要直接去写/或者

Java 中提供了一个跨平台的方法来获取路径分隔符,即使用 File.separator,这个属性会根据操作系统自动返回正确的路径分隔符。

File 类的注意点:

  1. 1. 一个 File 对象代表硬盘中实际存在的一个文件或者目录。

  2. 2. File 类的构造方法不会检验这个文件或目录是否真实存在,因此无论该路径下是否存在文件或者目录,都不影响 File 对象的创建。

2.2.3方法

File 的常用方法主要分为获取功能、获取绝对路径和相对路径、判断功能、创建删除功能的方法。

Java学习十—IO
image

)创建、删除功能的方法

  • • createNewFile() :文件不存在,创建一个新的空文件并返回true,文件存在,不创建文件并返回false

  • • delete() :删除文件或目录。如果是目录,只有目录为空才能删除。

  • • mkdir() :只能创建一级目录,如果父目录不存在,则创建失败。返回 true 表示创建成功,返回 false 表示创建失败。

  • • mkdirs() :可以创建多级目录,如果父目录不存在,则会一并创建。返回 true 表示创建成功,返回 false 表示创建失败或目录已经存在。

开发中一般用mkdirs();

方法测试,代码如下:

// 创建文件
File file = new File("/Users/username/example/test.txt");
if (file.createNewFile()) {
    System.out.println("创建文件成功:" + file.getAbsolutePath());
} else {
    System.out.println("创建文件失败:" + file.getAbsolutePath());
}

// 删除文件
if (file.delete()) {
    System.out.println("删除文件成功:" + file.getAbsolutePath());
} else {
    System.out.println("删除文件失败:" + file.getAbsolutePath());
}

// 创建多级目录
File directory = new File("/Users/username/example/subdir1/subdir2");
if (directory.mkdirs()) {
    System.out.println("创建目录成功:" + directory.getAbsolutePath());
} else {
    System.out.println("创建目录失败:" + directory.getAbsolutePath());
}

)获取功能的方法

1、getAbsolutePath() :返回此 File 的绝对路径。

2、getPath() :结果和 getAbsolutePath 一致。

3、getName() :返回文件名或目录名。

4、length() :返回文件长度,以字节为单位。

测试代码如下【注意测试以你自己的电脑文件夹为准】:

File f = new File("/Users/username/aaa/bbb.java");
System.out.println("文件绝对路径:"+f.getAbsolutePath());
System.out.println("文件构造路径:"+f.getPath());
System.out.println("文件名称:"+f.getName());
System.out.println("文件长度:"+f.length()+"字节");

File f2 = new File("/Users/username/aaa");
System.out.println("目录绝对路径:"+f2.getAbsolutePath());
System.out.println("目录构造路径:"+f2.getPath());
System.out.println("目录名称:"+f2.getName());
System.out.println("目录长度:"+f2.length());

注意:length() 表示文件的长度,File 对象表示目录的时候,返回值并无意义。

)绝对路径和相对路径

绝对路径是从文件系统的根目录开始的完整路径,它描述了一个文件或目录在文件系统中的确切位置。在 Windows 系统中,绝对路径通常以盘符(如 C:)开始,例如 “C:Program FilesJavajdk1.8.0_291binjava.exe“。在 macOS 和 Linux 系统中,绝对路径通常以斜杠(/)开始,例如 “/usr/local/bin/python3“。

相对路径是相对于当前工作目录的路径,它描述了一个文件或目录与当前工作目录之间的位置关系。在 Java 中,相对路径通常是相对于当前 Java 程序所在的目录,例如 “config/config.properties“。如果当前工作目录是 “/Users/username/project“,那么相对路径 “config/config.properties” 就表示 “/Users/username/project/config/config.properties“。

注意:

  • • 在 Windows 操作系统中,文件系统默认是不区分大小写的,即在文件系统中,文件名和路径的大小写可以混合使用。例如,”C:UsersusernameDocumentsexample.txt” 和 “C:UsersUsernameDocumentsExample.txt” 表示的是同一个文件。但是,Windows 操作系统提供了一个区分大小写的选项,可以在格式化磁盘时选择启用,这样文件系统就会区分大小写。

  • • 在 macOS 和 Linux 等 Unix 系统中,文件系统默认是区分大小写的。例如,在 macOS 系统中,”/Users/username/Documents/example.txt” 和 “/Users/username/Documents/Example.txt” 表示的是两个不同的文件。

// 绝对路径示例
File absoluteFile = new File("/Users/username/example/test.txt");
System.out.println("绝对路径:" + absoluteFile.getAbsolutePath());

// 相对路径示例
File relativeFile = new File("example/test.txt");
System.out.println("相对路径:" + relativeFile.getPath());

)判断功能的方法

1、 exists() :判断文件或目录是否存在。

2、 isDirectory() :判断是否为目录。

3、isFile() :判断是否为文件。

方法演示,代码如下:

File file = new File("/Users/username/example");

// 判断文件或目录是否存在
if (file.exists()) {
    System.out.println("文件或目录存在");
} else {
    System.out.println("文件或目录不存在");
}

// 判断是否是目录
if (file.isDirectory()) {
    System.out.println("是目录");
} else {
    System.out.println("不是目录");
}

// 判断是否是文件
if (file.isFile()) {
    System.out.println("是文件");
} else {
    System.out.println("不是文件");
}

5)目录的遍历

  • • String[] list() :返回一个 String 数组,表示该 File 目录中的所有子文件或目录。

  • • File[] listFiles() :返回一个 File 数组,表示该 File 目录中的所有的子文件或目录。

File directory = new File("/Users/itwanger/Documents/Github/paicoding");

// 列出目录下的文件名
String[] files = directory.list();
System.out.println("目录下的文件名:");
for (String file : files) {
    System.out.println(file);
}

// 列出目录下的文件和子目录
File[] filesAndDirs = directory.listFiles();
System.out.println("目录下的文件和子目录:");
for (File fileOrDir : filesAndDirs) {
    if (fileOrDir.isFile()) {
        System.out.println("文件:" + fileOrDir.getName());
    } else if (fileOrDir.isDirectory()) {
        System.out.println("目录:" + fileOrDir.getName());
    }
}

listFiles在获取指定目录下的文件或者子目录时必须满足下面两个条件:

    1. 1. 指定的目录必须存在

    1. 1. 指定的必须是目录。否则容易引发 NullPointerException 异常

6)递归遍历

不说啥了,直接上代码:

public static void main(String[] args) {
    File directory = new File("/Users/itwanger/Documents/Github/paicoding");

    // 递归遍历目录下的文件和子目录
    traverseDirectory(directory);
}

public static void traverseDirectory(File directory) {
    // 列出目录下的所有文件和子目录
    File[] filesAndDirs = directory.listFiles();

    // 遍历每个文件和子目录
    for (File fileOrDir : filesAndDirs) {
        if (fileOrDir.isFile()) {
            // 如果是文件,输出文件名
            System.out.println("文件:" + fileOrDir.getName());
        } else if (fileOrDir.isDirectory()) {
            // 如果是目录,递归遍历子目录
            System.out.println("目录:" + fileOrDir.getName());
            traverseDirectory(fileOrDir);
        }
    }
}

2.3RandomAccessFile类

2.3.1简介

RandomAccessFile 是 Java 中一个非常特殊的类,它既可以用来读取文件,也可以用来写入文件。与其他 IO 类(如 FileInputStream 和 FileOutputStream)不同,RandomAccessFile 允许您跳转到文件的任何位置,从那里开始读取或写入。这使得它特别适用于需要在文件中随机访问数据的场景,如数据库系统。

2.3.2示例

下面是一个使用 RandomAccessFile 的示例,包括写入和读取文件:

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

public class RandomAccessFileDemo {

    public static void main(String[] args) {
        String filePath = "logs/javabetter/itwanger.txt";

        try {
            // 使用 RandomAccessFile 写入文件
            writeToFile(filePath, "Hello, 沉默王二!");

            // 使用 RandomAccessFile 读取文件
            String content = readFromFile(filePath);
            System.out.println("文件内容: " + content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void writeToFile(String filePath, String content) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw")) {
            // 将文件指针移动到文件末尾(在此处追加内容)
            randomAccessFile.seek(randomAccessFile.length());

            // 写入内容
            randomAccessFile.writeUTF(content);
        }
    }

    private static String readFromFile(String filePath) throws IOException {
        StringBuilder content = new StringBuilder();

        try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r")) {
            // 将文件指针移动到文件开始处(从头开始读取)
            randomAccessFile.seek(0);

            content.append(randomAccessFile.readUTF());
        }

        return content.toString();
    }
}

为了避免中文乱码问题,我们使用 RandomAccessFile 的 writeUTF 和 readUTF 方法,它们将使用 UTF-8 编码处理字符串。大家可以运行一下这段代码,体验一下。

Java学习十—IO

接下来,会详细介绍一下 RandomAccessFile 的构造方法和常用的方法。

2.3.3构造方法

RandomAccessFile 主要有两个构造方法:

  • • RandomAccessFile(File file, String mode):使用给定的文件对象和访问模式创建一个新的 RandomAccessFile 实例。

  • • RandomAccessFile(String name, String mode):使用给定的文件名和访问模式创建一个新的 RandomAccessFile 实例。

访问模式 mode 的值可以是:

  • • “r”:以只读模式打开文件。调用结果对象的任何 write 方法都将导致 IOException。

  • • “rw”:以读写模式打开文件。如果文件不存在,它将被创建。

  • • “rws”:以读写模式打开文件,并要求对内容或元数据的每个更新都被立即写入到底层存储设备。这种模式是同步的,可以确保在系统崩溃时不会丢失数据。

  • • “rwd”:与“rws”类似,以读写模式打开文件,但仅要求对文件内容的更新被立即写入。元数据可能会被延迟写入。

2.3.4方法

  • • long getFilePointer():返回文件指针的当前位置。

  • • long length():返回此文件的长度。

  • • int read():从该文件中读取一个字节数据。

  • • int read(byte[] b):从该文件中读取字节数据并将其存储到指定的字节数组中。

  • • int read(byte[] b, int off, int len):从该文件中读取字节数据并将其存储到指定的字节数组中,从偏移量 off 开始,最多读取 len 个字节。

  • • String readLine():从该文件中读取一行文本。

  • • readUTF():从文件读取 UTF-8 编码的字符串。此方法首先读取两个字节的长度信息,然后根据这个长度读取字符串的 UTF-8 字节。最后,这些字节被转换为 Java 字符串。这意味着当你使用 readUTF 方法读取字符串时,需要确保文件中的字符串是使用 writeUTF 方法写入的,这样它们之间的长度信息和编码方式才能保持一致。

  • • void seek(long pos):将文件指针设置到文件中的 pos 位置。

  • • void write(byte[] b):将指定的字节数组的所有字节写入该文件。

  • • void write(byte[] b, int off, int len):将指定字节数组的部分字节写入该文件,从偏移量 off 开始,写入 len 个字节。

  • • void write(int b):将指定的字节写入该文件。

  • • writeUTF(String str):将一个字符串以 UTF-8 编码写入文件。此方法首先写入两个字节的长度信息,表示字符串的 UTF-8 字节长度,然后写入 UTF-8 字节本身。因此,当你使用 writeUTF 写入字符串时,实际写入的字节数会比字符串的 UTF-8 字节长度多两个字节。这两个字节用于在读取字符串时确定正确的字符串长度。

再来看一个示例,结合前面的讲解,就会彻底掌握 RandomAccessFile。

File file = new File("logs/javabetter/itwanger.txt");

try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
    // 写入文件
    raf.writeUTF("Hello, 沉默王二!");

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

    // 读取文件内容
    String content = raf.readUTF();
    System.out.println("内容: " + content);

} catch (IOException e) {
    e.printStackTrace();
}

在这个示例中,我们首先创建了一个名为 itwanger.txt 的文件对象。然后我们使用 RandomAccessFile 以读写模式打开这个文件。

接下来,我们使用 writeUTF 方法将字符串”Hello, 沉默王二!”写入文件。然后,我们使用 seek 方法将文件指针移动到文件开头,并使用 readUTF 方法读取文件内容。输出应该是”Hello, 沉默王二!”。

最后,我们使用try-with-resources语句确保 RandomAccessFile 在操作完成后被正确关闭。

2.4Apache FileUtils 类

2.4.1简介

FileUtils 类是 Apache Commons IO 库中的一个类,提供了一些更为方便的方法来操作文件或目录。

2.4.2方法

1)复制文件或目录:

File srcFile = new File("path/to/src/file");
File destFile = new File("path/to/dest/file");
// 复制文件
FileUtils.copyFile(srcFile, destFile);
// 复制目录
FileUtils.copyDirectory(srcFile, destFile);

2)删除文件或目录:

File file = new File("path/to/file");
// 删除文件或目录
FileUtils.delete(file);

需要注意的是,如果要删除一个非空目录,需要先删除目录中的所有文件和子目录。

3)移动文件或目录:

File srcFile = new File("path/to/src/file");
File destFile = new File("path/to/dest/file");
// 移动文件或目录
FileUtils.moveFile(srcFile, destFile);

4)查询文件或目录的信息:

File file = new File("path/to/file");
// 获取文件或目录的修改时间
Date modifyTime = FileUtils.lastModified(file);
// 获取文件或目录的大小
long size = FileUtils.sizeOf(file);
// 获取文件或目录的扩展名
String extension = FileUtils.getExtension(file.getName());

2.5Hutool FileUtil 类

2.5.1简介

FileUtil 类是 Hutool工具包中的文件操作工具类,提供了一系列简单易用的文件操作方法,可以帮助 Java 开发者快速完成文件相关的操作任务。

2.5.2方法

FileUtil 类包含以下几类操作工具:

  • • 文件操作:包括文件目录的新建、删除、复制、移动、改名等

  • • 文件判断:判断文件或目录是否非空,是否为目录,是否为文件等等。

  • • 绝对路径:针对 ClassPath 中的文件转换为绝对路径文件。

  • • 文件名:主文件名,扩展名的获取

  • • 读操作:包括 getReader、readXXX 操作

  • • 写操作:包括 getWriter、writeXXX 操作

下面是 FileUtil 类中一些常用的方法:

1、copyFile:复制文件。该方法可以将指定的源文件复制到指定的目标文件中。

File dest = FileUtil.file("FileUtilDemo2.java");
FileUtil.copyFile(file, dest);

2、move:移动文件或目录。该方法可以将指定的源文件或目录移动到指定的目标文件或目录中。

FileUtil.move(file, dest, true);

3、del:删除文件或目录。该方法可以删除指定的文件或目录,如果指定的文件或目录不存在,则会抛出异常。

FileUtil.del(file);

4、rename:重命名文件或目录。该方法可以将指定的文件或目录重命名为指定的新名称。

FileUtil.rename(file, "FileUtilDemo3.java", true);

5、readLines:从文件中读取每一行数据。

FileUtil.readLines(file, "UTF-8").forEach(System.out::println);

更多方法,可以去看一下 hutool 的源码,里面有非常多实用的方法,多看看,绝对能提升不少编程水平。

三、字节流

3.1关于字节流

Java 字节流是 Java I/O API 中用于处理字节数据的一种机制。它们提供了一种读取和写入字节数据的方式,适用于处理诸如图像、音频、视频等二进制数据的情况。

3.2字节输入流(InputStream)

3.2.1简介

java.io.InputStream字节输入流超类(父类),我们来看一下它的一些共性方法:

1、close() :关闭此输入流并释放与此流相关的系统资源。

2、int read(): 从输入流读取数据的下一个字节。

3、read(byte[] b): 该方法返回的 int 值代表的是读取了多少个字节,读到几个返回几个,读取不到返回-1

3.2.2FileInputStream类

InputStream 有很多子类,我们从最简单的一个子类 FileInputStream 开始。看名字就知道是文件输入流,用于将数据从文件中读取数据。

构造方法

1、FileInputStream(String name):创建一个 FileInputStream 对象,并打开指定名称的文件进行读取。文件名由 name 参数指定。如果文件不存在,将会抛出 FileNotFoundException 异常。

2、FileInputStream(File file):创建一个 FileInputStream 对象,并打开指定的 File 对象表示的文件进行读取。

代码示例如下:

// 创建一个 FileInputStream 对象
FileInputStream fis = new FileInputStream("test.txt");

// 读取文件内容
int data;
while ((data = fis.read()) != -1) {
    System.out.print((char) data);
}

// 关闭输入流
fis.close();

读取字节数据

①、读取字节read()方法会读取一个字节并返回其整数表示。如果已经到达文件的末尾,则返回 -1。如果在读取时发生错误,则会抛出 IOException 异常。

代码示例如下:

// 创建一个 FileInputStream 对象
FileInputStream fis = new FileInputStream("test.txt");

// 读取文件内容
int data;
while ((data = fis.read()) != -1) {
    System.out.print((char) data);
}

// 关闭输入流
fis.close();

②、使用字节数组读取read(byte[] b) 方法会从输入流中最多读取 b.length 个字节,并将它们存储到缓冲区数组 b 中。

代码示例如下:

// 创建一个 FileInputStream 对象
FileInputStream fis = new FileInputStream("test.txt");

// 读取文件内容到缓冲区
byte[] buffer = new byte[1024];
int count;
while ((count = fis.read(buffer)) != -1) {
    System.out.println(new String(buffer, 0, count));
}

// 关闭输入流
fis.close();

复制图片

原理很简单,就是把图片信息读入到字节输入流中,再通过字节输出流写入到文件中。

代码示例如下所示:

// 创建一个 FileInputStream 对象以读取原始图片文件
FileInputStream fis = new FileInputStream("original.jpg");

// 创建一个 FileOutputStream 对象以写入复制后的图片文件
FileOutputStream fos = new FileOutputStream("copy.jpg");

// 创建一个缓冲区数组以存储读取的数据
byte[] buffer = new byte[1024];
int count;

// 读取原始图片文件并将数据写入复制后的图片文件
while ((count = fis.read(buffer)) != -1) {
    fos.write(buffer, 0, count);
}

// 关闭输入流和输出流
fis.close();
fos.close();

上面的代码创建了一个 FileInputStream 对象以读取原始图片文件,并创建了一个 FileOutputStream 对象以写入复制后的图片文件。然后,使用 while 循环逐个读取原始图片文件中的字节,并将其写入复制后的图片文件中。最后,关闭输入流和输出流释放资源。

小结

InputStream 是字节输入流的抽象类,它定义了读取字节数据的方法,如 read()read(byte[] b)read(byte[] b, int off, int len) 等。OutputStream 是字节输出流的抽象类,它定义了写入字节数据的方法,如 write(int b)write(byte[] b)write(byte[] b, int off, int len) 等。这两个抽象类是字节流的基础。

FileInputStream 是从文件中读取字节数据的流,它继承自 InputStream。FileOutputStream 是将字节数据写入文件的流,它继承自 OutputStream。这两个类是字节流最常用的实现类之一。

3.3字节输出流(OutputStream)

3.3.1简介

java.io.OutputStream字节输出流超类(父类),我们来看一下它定义的一些共性方法:

1、 close() :关闭此输出流并释放与此流相关联的系统资源。

2、 flush() :刷新此输出流并强制缓冲区的字节被写入到目的地。

3、 write(byte[] b):将 b.length 个字节从指定的字节数组写入此输出流。

4、 write(byte[] b, int off, int len) :从指定的字节数组写入 len 字节到此输出流,从偏移量 off开始。 也就是说从off个字节数开始一直到len个字节结束

3.3.2FileOutputStream类

OutputStream 有很多子类,我们从最简单的一个子类 FileOutputStream 开始。看名字就知道是文件输出流,用于将数据写入到文件。

构造方法

1、使用文件名创建 FileOutputStream 对象。

String fileName = "example.txt";
FileOutputStream fos = new FileOutputStream(fileName);

以上代码使用文件名 “example.txt” 创建一个 FileOutputStream 对象,将数据写入到该文件中。如果文件不存在,则创建一个新文件;如果文件已经存在,则覆盖原有文件

2、使用文件对象创建 FileOutputStream 对象。

File file = new File("example.txt");
FileOutputStream fos = new FileOutputStream(file);

FileOutputStream 的使用示例:

FileOutputStream fos = null;
try {
  fos = new FileOutputStream("example.txt");
  fos.write("沉默王二".getBytes());
} catch (IOException e) {
  e.printStackTrace();
} finally {
  if (fos != null) {
    try {
      fos.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

以上代码创建了一个 FileOutputStream 对象,将字符串 “沉默王二” 写入到 example.txt 文件中,并在最后关闭了输出流。

写入字节数据

使用 FileOutputStream 写入字节数据主要通过 write 方法:

write(int b)
write(byte[] b)
write(byte[] b,int off,int len)  //从`off`索引开始,`len`个字节

①、写入字节write(int b) 方法,每次可以写入一个字节,代码如下:

// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");   
// 写出数据
fos.write(97); // 第1个字节
fos.write(98); // 第2个字节
fos.write(99); // 第3个字节
// 关闭资源
fos.close();

字符 a 的 ASCII 值open in new window为 97,字符 b 的ASCII 值为 98,字符 b 的ASCII 值为 99。也就是说,以上代码可以写成:

// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");   
// 写出数据
fos.write('a'); // 第1个字节
fos.write('b'); // 第2个字节
fos.write('c'); // 第3个字节
// 关闭资源
fos.close();

当使用 write(int b) 方法写出一个字节时,参数 b 表示要写出的字节的整数值。由于一个字节只有8位,因此参数 b 的取值范围应该在 0 到 255 之间,超出这个范围的值将会被截断。例如,如果参数 b 的值为 -1,那么它会被截断为 255,如果参数 b 的值为 256,那么它会被截断为 0。

在将参数 b 写入输出流中时,write(int b) 方法只会将参数 b 的低8位写入,而忽略高24位。这是因为在 Java 中,整型类型(包括 byte、short、int、long)在内存中以二进制补码形式表示。当将一个整型值传递给 write(int b) 方法时,方法会将该值转换为 byte 类型,只保留二进制补码的低8位,而忽略高24位。

例如,如果要写出的整数为 0x12345678,它的二进制补码表示为 0001 0010 0011 0100 0101 0110 0111 1000。当使用 write(int b) 方法写出该整数时,只会将二进制补码的低8位 0111 1000 写出,而忽略高24位 0001 0010 0011 0100 0101 0110。这就是参数 b 的高24位被忽略的原因。

0111 1000 是一个8位的二进制数,它对应的十进制数是 120,对应的 ASCII 码字符是小写字母 “x”。在 ASCII 码表中,小写字母 “x” 的十进制 ASCII 码值为 120。因此,如果使用 write(int b) 方法写出一个字节值为 0x78(十进制为 120),那么写出的结果就是小写字母 “x”。

我们来验证一下:

FileOutputStream fos = null;
try {
    fos = new FileOutputStream("example.txt");

    fos.write(120);
    fos.write('x');
    fos.write(0x12345678);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fos != null) {
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

来看一下结果:

Java学习十—IO

果然是 3 个 x。

②、写入字节数组write(byte[] b),代码示例:

// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");   
// 字符串转换为字节数组
byte[] b = "沉默王二有点帅".getBytes();
// 写入字节数组数据
fos.write(b);
// 关闭资源
fos.close();

③、写入指定长度字节数组write(byte[] b, int off, int len),代码示例:

// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");   
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
fos.write(b,2,2);
// 关闭资源
fos.close();

实现数据追加、换行

在上面的代码示例中,每次运行程序都会创建新的输出流对象,于是文件中的数据也会被清空。如果想保留目标文件中的数据,还能继续追加新数据,该怎么办呢?以及如何实现换行呢?

其实很简单。

我们来学习FileOutputStream的另外两个构造方法,如下:

1、使用文件名和追加标志创建 FileOutputStream 对象

String fileName = "example.txt";
boolean append = true;
FileOutputStream fos = new FileOutputStream(fileName, append);

以上代码使用文件名 “example.txt” 和追加标志创建一个 FileOutputStream 对象,将数据追加到该文件的末尾。如果文件不存在,则创建一个新文件;如果文件已经存在,则在文件末尾追加数据。

2、使用文件对象和追加标志创建 FileOutputStream 对象

File file = new File("example.txt");
boolean append = true;
FileOutputStream fos = new FileOutputStream(file, append);

以上代码使用文件对象和追加标志创建一个 FileOutputStream 对象,将数据追加到该文件的末尾。

这两个构造方法,第二个参数中都需要传入一个boolean类型的值,true 表示追加数据,false 表示不追加也就是清空原有数据。

实现数据追加代码如下:

// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt",true);   
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
fos.write(b);
// 关闭资源
fos.close();

多次运行代码,你会发现数据在不断地追加。

在 Windows 系统中,换行符号是rn,具体代码如下:

String filename = "example.txt";
FileOutputStream fos = new FileOutputStream(filename, true);  // 追加模式
String content = "沉默王二rn";  // 使用回车符和换行符的组合
fos.write(content.getBytes());
fos.close();

在 macOS 系统中,换行符是 n,具体代码如下:

String filename = "example.txt";
FileOutputStream fos = new FileOutputStream(filename, true);  // 追加模式
String content = "沉默王二n";  // 只使用换行符
fos.write(content.getBytes());
fos.close();

这里再唠一唠回车符和换行符。

回车符(r)和换行符(n)是计算机中常见的控制字符,用于表示一行的结束或者换行的操作。它们在不同的操作系统和编程语言中的使用方式可能有所不同。

在 Windows 系统中,通常使用回车符和换行符的组合(rn)来表示一行的结束。在文本文件中,每行的末尾都会以一个回车符和一个换行符的组合结束。这是由于早期的打印机和终端设备需要回车符和换行符的组合来完成一行的结束和换行操作。在 Windows 中,文本编辑器和命令行终端等工具都支持使用回车符和换行符的组合来表示一行的结束。

而在 macOS 和 Linux 系统中,通常只使用换行符(n)来表示一行的结束。在文本文件中,每行的末尾只有一个换行符。这是由于早期 Unix 系统中的终端设备只需要换行符来完成一行的结束和跨行操作。在 macOS 和 Linux 中,文本编辑器和终端等工具都支持使用换行符来表示一行的结束。

在编程语言中,通常也会使用回车符和换行符来进行字符串的操作。例如,在 Java 中,字符串中的回车符可以用 “r” 来表示,换行符可以用 “n” 来表示。在通过输入输出流进行文件读写时,也需要注意回车符和换行符的使用方式和操作系统的差异。

四、字符流

4.1简介

字符流 Reader 和 Writer 的故事要从它们的类关系图开始,来看图。

Java学习十—IO

字符流是一种用于读取和写入字符数据的输入输出流。与字节流不同,字符流以字符为单位读取和写入数据,而不是以字节为单位。常用来处理文本信息。

如果用字节流直接读取中文,可能会遇到乱码问题,见下例:

//FileInputStream为操作文件的字符输入流
FileInputStream inputStream = new FileInputStream("a.txt");//内容为“沉默王二是傻 X”

int len;
while ((len=inputStream.read())!=-1){
    System.out.print((char)len);
}

来看运行结果:

运行结果:   æ²‰é»˜çŽ‹äºŒæ˜¯å‚» X

看一下截图:

Java学习十—IO

之所以出现乱码是因为在字节流中,一个字符通常由多个字节组成,而不同的字符编码使用的字节数不同。如果我们使用了错误的字符编码,或者在读取和写入数据时没有正确处理字符编码的转换,就会导致读取出来的中文字符出现乱码。

例如,当我们使用默认的字符编码(见上例)读取一个包含中文字符的文本文件时,就会出现乱码。因为默认的字符编码通常是 ASCII 编码,它只能表示英文字符,而不能正确地解析中文字符。

那使用字节流该如何正确地读出中文呢?见下例。

try (FileInputStream inputStream = new FileInputStream("a.txt")) {
    byte[] bytes = new byte[1024];
    int len;
    while ((len = inputStream.read(bytes)) != -1) {
        System.out.print(new String(bytes, 0, len));
    }
}

为什么这种方式就可以呢?

因为我们拿 String 类进行了解码,查看new String(byte bytes[], int offset, int length)的源码就可以发现,该构造方法有解码功能:

public String(byte bytes[], int offset, int length) {
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(bytes, offset, length);
}

继续追看 StringCoding.decode() 方法调用的 defaultCharset() 方法,会发现默认编码是UTF-8,代码如下

public static Charset defaultCharset() {
    if (defaultCharset == null) {
        synchronized (Charset.class) {
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}
static char[] decode(byte[] ba, int off, int len) {
    String csn = Charset.defaultCharset().name();
    try {
        // use charset name decode() variant which provides caching.
        return decode(csn, ba, off, len);
    } catch (UnsupportedEncodingException x) {
        warnUnsupportedCharset(csn);
    }
}

在 Java 中,常用的字符编码有 ASCII、ISO-8859-1、UTF-8、UTF-16 等。其中,ASCII 和 ISO-8859-1 只能表示部分字符,而 UTF-8 和 UTF-16 可以表示所有的 Unicode 字符,包括中文字符。

当我们使用 new String(byte bytes[], int offset, int length) 将字节流转换为字符串时,Java 会根据 UTF-8 的规则将每 3 个字节解码为一个中文字符,从而正确地解码出中文。

尽管字节流也有办法解决乱码问题,但不够直接,于是就有了字符流,专门用于处理文本文件(音频、图片、视频等为非文本文件)。

从另一角度来说:字符流 = 字节流 + 编码表

4.2字符输入流(Reader)

4.2.1简介

java.io.Reader字符输入流超类(父类),它定义了字符输入流的一些共性方法:

  • • 1、close():关闭此流并释放与此流相关的系统资源。

  • • 2、read():从输入流读取一个字符。

  • • 3、read(char[] cbuf):从输入流中读取一些字符,并将它们存储到字符数组 cbuf

4.2.2FileReader类

FileReader 是 Reader 的子类,用于从文件中读取字符数据。它的主要特点如下:

  • • 可以通过构造方法指定要读取的文件路径。

  • • 每次可以读取一个或多个字符。

  • • 可以读取 Unicode 字符集中的字符,通过指定字符编码来实现字符集的转换。

构造方法

  • • 1、FileReader(File file):创建一个新的 FileReader,参数为File对象

  • • 2、FileReader(String fileName):创建一个新的 FileReader,参数为文件名。

代码示例如下:

// 使用File对象创建流对象
File file = new File("a.txt");
FileReader fr = new FileReader(file);

// 使用文件名称创建流对象
FileReader fr = new FileReader("b.txt");

读取字符数据

①、读取字符read方法,每次可以读取一个字符,返回读取的字符(转为 int 类型),当读取到文件末尾时,返回-1。代码示例如下:

// 使用文件名称创建流对象
FileReader fr = new FileReader("abc.txt");
// 定义变量,保存数据
int b;
// 循环读取
while ((= fr.read())!=-1) {
    System.out.println((char)b);
}
// 关闭资源
fr.close();

②、读取指定长度的字符read(char[] cbuf, int off, int len),并将其存储到字符数组中。其中,cbuf 表示存储读取结果的字符数组,off 表示存储结果的起始位置,len 表示要读取的字符数。代码示例如下:

File textFile = new File("docs/约定.md");
// 给一个 FileReader 的示例
// try-with-resources FileReader
try(FileReader reader = new FileReader(textFile);) {
    // read(char[] cbuf)
    char[] buffer = new char[1024];
    int len;
    while ((len = reader.read(buffer, 0, buffer.length)) != -1) {
        System.out.print(new String(buffer, 0, len));
    }
}

在这个例子中,使用 FileReader 从文件中读取字符数据,并将其存储到一个大小为 1024 的字符数组中。每次读取 len 个字符,然后使用 String 构造方法将其转换为字符串并输出。

FileReader 实现了 AutoCloseable 接口,因此可以使用 try-with-resourcesopen in new window 语句自动关闭资源,避免了手动关闭资源的繁琐操作。

4.3字符输出流(Writer)

4.3.1简介

java.io.Writer字符输出流类的超类(父类),可以将指定的字符信息写入到目的地,来看它定义的一些共性方法:

  • • 1、write(int c) 写入单个字符。

  • • 2、write(char[] cbuf) 写入字符数组。

  • • 3、write(char[] cbuf, int off, int len) 写入字符数组的一部分,off为开始索引,len为字符个数。

  • • 4、write(String str) 写入字符串。

  • • 5、write(String str, int off, int len) 写入字符串的某一部分,off 指定要写入的子串在 str 中的起始位置,len 指定要写入的子串的长度。

  • • 6、flush() 刷新该流的缓冲。

  • • 7、close() 关闭此流,但要先刷新它。

4.3.2FileWriter类

java.io.FileWriter 类是 Writer 的子类,用来将字符写入到文件。

构造方法

  • • FileWriter(File file): 创建一个新的 FileWriter,参数为要读取的File对象。

  • • FileWriter(String fileName): 创建一个新的 FileWriter,参数为要读取的文件的名称。

代码示例如下:

// 第一种:使用File对象创建流对象
File file = new File("a.txt");
FileWriter fw = new FileWriter(file);

// 第二种:使用文件名称创建流对象
FileWriter fw = new FileWriter("b.txt");

写入数据

①、写入字符write(int b) 方法,每次可以写出一个字符,代码示例如下:

FileWriter fw = null;
try {
    fw = new FileWriter("output.txt");
    fw.write(72); // 写入字符'H'的ASCII码
    fw.write(101); // 写入字符'e'的ASCII码
    fw.write(108); // 写入字符'l'的ASCII码
    fw.write(108); // 写入字符'l'的ASCII码
    fw.write(111); // 写入字符'o'的ASCII码
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (fw != null) {
            fw.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

在这个示例代码中,首先创建一个 FileWriter 对象 fw,并指定要写入的文件路径 “output.txt”。然后使用 fw.write() 方法将字节写入文件中,这里分别写入字符’H’、’e’、’l’、’l’、’o’的 ASCII 码。最后在 finally 块中关闭 FileWriter 对象,释放资源。

需要注意的是,使用 write(int b) 方法写入的是一个字节,而不是一个字符。如果需要写入字符,可以使用 write(char cbuf[]) 或 write(String str) 方法。

②、写入字符数组write(char[] cbuf) 方法,将指定字符数组写入输出流。代码示例如下:

FileWriter fw = null;
try {
    fw = new FileWriter("output.txt");
    char[] chars = {'H', 'e', 'l', 'l', 'o'};
    fw.write(chars); // 将字符数组写入文件
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (fw != null) {
            fw.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

③、写入指定字符数组write(char[] cbuf, int off, int len) 方法,将指定字符数组的一部分写入输出流。代码示例如下(重复的部分就不写了哈,参照上面的部分):

fw = new FileWriter("output.txt");
    char[] chars = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!'};
fw.write(chars, 0, 5); // 将字符数组的前 5 个字符写入文件

使用 fw.write() 方法将字符数组的前 5 个字符写入文件中。

④、写入字符串write(String str) 方法,将指定字符串写入输出流。代码示例如下:

fw = new FileWriter("output.txt");
String str = "沉默王二";
fw.write(str); // 将字符串写入文件

⑤、写入指定字符串write(String str, int off, int len) 方法,将指定字符串的一部分写入输出流。代码示例如下(try-with-resources形式):

String str = "沉默王二真的帅啊!";
try (FileWriter fw = new FileWriter("output.txt")) {
    fw.write(str, 0, 5); // 将字符串的前 5 个字符写入文件
} catch (IOException e) {
    e.printStackTrace();
}

【注意】如果不关闭资源,数据只是保存到缓冲区,并未保存到文件中。

关闭close和刷新flush

因为 FileWriter 内置了缓冲区 ByteBuffer,所以如果不关闭输出流,就无法把字符写入到文件中。

Java学习十—IO

但是关闭了流对象,就无法继续写数据了。如果我们既想写入数据,又想继续使用流,就需要 flush 方法了。

flush :刷新缓冲区,流对象可以继续使用。

close :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

flush还是比较有趣的,来段代码体会体会:

//源   也就是输入流【读取流】 读取a.txt文件
FileReader fr=new FileReader("abc.txt");  //必须要存在a.txt文件,否则报FileNotFoundException异常
//目的地  也就是输出流
FileWriter fw=new FileWriter("b.txt");  //系统会自动创建b.txt,因为它是输出流!
int len;
while((len=fr.read())!=-1){
    fw.write(len);
}
//注意这里是没有使用close关闭流,开发中不能这样做,但是为了更好的体会flush的作用

运行效果是怎么样的呢?答案是b.txt文件中依旧是空的,并没有任何东西。

Java学习十—IO

原因我们前面已经说过了。编程就是这样,不去敲,永远学不会!!!所以一定要去敲,多敲啊!!!

在以上的代码中再添加下面三句代码,b.txt文件就能复制到源文件的数据了!

fr.close();
fw.flush();
fw.close();

flush()这个方法是清空缓存的意思,用于清空缓冲区的数据流,进行流的操作时,数据先被读到内存中,然后再把数据写到文件中。

你可以使用下面的代码示例再体验一下:

// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 写出数据,通过flush
fw.write('刷'); // 写出第1个字符
fw.flush();
fw.write('新'); // 继续写出第2个字符,写出成功
fw.flush();

// 写出数据,然后close
fw.write('关'); // 写出第1个字符
fw.close();
fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
fw.close();

注意,即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。当然你也可以用 try-with-resources 的方式。

续写和换行

续写和换行:操作类似于FileOutputStream操作,直接上代码:

// 使用文件名称创建流对象,可以续写数据
FileWriter fw = new FileWriter("fw.txt",true);   
// 写出字符串
fw.write("沉默王二");
// 写出换行
fw.write("rn");
// 写出字符串
fw.write("是傻 X");
// 关闭资源
fw.close();

输出结果如下所示:

输出结果:
沉默王二
是傻 X

文本文件复制

直接上代码:

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

public class CopyFile {
    public static void main(String[] args) throws IOException {
        //创建输入流对象
        FileReader fr=new FileReader("aa.txt");//文件不存在会抛出java.io.FileNotFoundException
        //创建输出流对象
        FileWriter fw=new FileWriter("copyaa.txt");
        /*创建输出流做的工作:
         *      1、调用系统资源创建了一个文件
         *      2、创建输出流对象
         *      3、把输出流对象指向文件  
         * */

        //文本文件复制,一次读一个字符
        copyMethod1(fr, fw);
        //文本文件复制,一次读一个字符数组
        copyMethod2(fr, fw);
  
        fr.close();
        fw.close();
    }

    public static void copyMethod1(FileReader fr, FileWriter fw) throws IOException {
        int ch;
        while((ch=fr.read())!=-1) {//读数据
            fw.write(ch);//写数据
        }
        fw.flush();
    }

    public static void copyMethod2(FileReader fr, FileWriter fw) throws IOException {
        char chs[]=new char[1024];
        int len=0;
        while((len=fr.read(chs))!=-1) {//读数据
            fw.write(chs,0,len);//写数据
        }
        fw.flush();
    }
}

4.3.3IO异常的处理

我们在学习的过程中可能习惯把异常抛出,而实际开发中建议使用try...catch...finally 代码块,处理异常部分,格式代码如下:

// 声明变量
FileWriter fw = null;
try {
    //创建流对象
    fw = new FileWriter("fw.txt");
    // 写出数据
    fw.write("二哥真的帅"); //哥敢摸si
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (fw != null) {
            fw.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

或者直接使用 try-with-resources 的方式。

try (FileWriter fw = new FileWriter("fw.txt")) {
    // 写出数据
    fw.write("二哥真的帅"); //哥敢摸si
} catch (IOException e) {
    e.printStackTrace();
}

在这个代码中,try-with-resources 会在 try 块执行完毕后自动关闭 FileWriter 对象 fw,不需要手动关闭流。如果在 try 块中发生了异常,也会自动关闭流并抛出异常。因此,使用 try-with-resources 可以让代码更加简洁、安全和易读。

4.3.4小结

Writer 和 Reader 是 Java I/O 中用于字符输入输出的抽象类,它们提供了一系列方法用于读取和写入字符数据。它们的区别在于 Writer 用于将字符数据写入到输出流中,而 Reader 用于从输入流中读取字符数据。

Writer 和 Reader 的常用子类有 FileWriter、FileReader,可以将字符流写入和读取到文件中。

在使用 Writer 和 Reader 进行字符输入输出时,需要注意字符编码的问题。


原文始发于微信公众号(技海拾贝):Java学习十—IO

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/295291.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!