4 - IO

目录

1. 总览

1.1 传输方式划分

1)InputStream 类

2)OutputStream 类

3)Reader类

4)Writer 类

1.2 操作对象划分

 1)文件

2)数组(内存)

3)管道

4)基本数据类型

5)缓冲

6)打印 

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

8)转换

2. 文件流 Java File

2.1 File 构造

2.2 常用方法

1)获取功能的方法

2)绝对路径与相对路径

3)判断功能的方法

4)创建,删除方法

5)目录遍历

6)递归遍历 

2.2 RandomAccessFile

1)构造方法

2)主要方法

2.3 Apache FileUtils 类

1)复制文件或者目录

2)删除文件或者目录

3)移动文件或者目录

4)查询文件或者目录信息

2.4 Hutool FileUtil 类

1)复制文件

2)移动文件或目录

3)删除文件或目录

4)重命名

5)读取每一行数据

3. 字节流

3.1 字节输出流

1)FileOutputStream 构造方法

2)FileOutputStream 写入字节数据

3)实现数据追加、换行

3.2 字节输入流

1)构造方法

2)读取字节数据

3)复制文件

4. 字符流

4.1 字符输入流

1)构造方法

2)读取数据

4.2 字符输出流

1)构造方法

2)写入数据

3)关闭、刷新

 4)续写与换行

5)文本文件的复制

5. 缓冲流

5.1 字节缓冲流

1)构造方法

2)缓冲流解决的问题

3)byte & 0xFF

 5.2 字符缓冲流

1)构造方法

2)特有方法

6. 转换流

6.1 编码与解码

6.2 字符集

 6.3 InputStreamReader

1)构造方法

2)解决编码

6.4 OutputStreamWriter

1)构造方法

2)使用

7. 序列流

7.1 ObjectOutputStream

1)构造方法

2)使用

7.2 ObjectInputStream

1)构造及使用

7.3 Kryo

8. 打印流

 printf 方法


IO 指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接

1. 总览

Java 中是通过流处理 IO 的,那么什么是流呢?

流(Stream),是一个抽象的概念,指一连串的数据,以先进先出的方式发送信息的通道

关于流的特性有下面几点:

  • 先进先出:最先写入输出流的数据最先被输入流读取到。
  • 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)
  • 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。

1.1 传输方式划分

传输方式有两种,字节和字符

  • 字节(byte)是计算机中用来表示存储容量的一个计量单位,一个字节有 8 位(bit)
  • 字符(char)可以是计算机中使用的字母、数字、和符号(通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节)

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

字节流和字符流的区别:

  • 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件
  • 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大

1)InputStream 类

  • int read(): 读取数据
  • int read(byte b[], int off, int len):从第 off 位置开始读,读取 len 长度的字节,然后放入数组b
  • long skip(long n):跳过指定个数的字节
  • int available():返回可读的字节数
  • void close():关闭流,释放资源

2)OutputStream 类

  • void write(int b):写入一个字节,虽然参数是一个 int,但只有低8位才会写入,高24位舍弃
  • void read(byte b[], int off, int len):将数组 b 中的从 off 位置开始,长度为 len 的字节写入
  • void flush(): 强制刷新,将缓冲区的数据写入
  • void close():关闭流

3)Reader类

  • int read():读取单个字符
  • int read(char cbuf[], int off, int len):从第 off 位置开始读,读取 len 长度的字符,然后放入b
  • long skip(long n):跳过指定个数的字符
  • int ready():是否可以读了
  • void close():关闭流,释放资源

4)Writer 类

  • void writer(int c):写入一个字符
  • void write(char cbuf[], int off, int len):将数组cbuf中的从 off 位置开始,长度为 len 字符写入
  • void flush(): 强制刷新,将缓冲区的数据写入
  • void close():关闭流

1.2 操作对象划分

  • Input:将外部的数据读入内存,比如把文件从硬盘读取到内存,从网络读取数据到内存
  • Output:将内存中的数据写入到外部,比如把数据从内存写入到文件,数据从内存输出到网络

 1)文件

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

FileInputStream 的例子:

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

// 循环读取文件中的数据
while ((b = 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 ((b = 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)管道

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

一个线程通过 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();

ObjectInputStream 和 ObjectOutputStream(用于读写对象)

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

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();
    }
}

5)缓冲

为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的

例如:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter

BufferedInputStream 读取文件的示例:

  1. 创建了一个 BufferedInputStream 对象,用于从文件中读取数据
  2. 创建了一个字节数组作为缓存区
  3. 通过 while 循环实现读取数据
  4. 每次读取数据后对缓存区中的数据进行处理
  5. 关闭 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();

使用 BufferedOutputStream 写入文件的示例:

  1. 创建了一个 BufferedOutputStream 对象,用于将数据写入到文件中
  2. 创建了一个字节数组作为缓存区
  3. 通过 write() 方法实现写入数据
  4. 通过 flush() 方法将缓存区中的数据写入到文件中
  5. 关闭,释放资源

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

// 创建一个 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();

使用 BufferedReader 读取文件的示例:

  1. 创建了一个 BufferedReader 对象,用于从文件中读取数据
  2. 使用 readLine() 方法读取文件中的数据
  3. 每次读取一行数据并将其存储到一个字符串中
  4. 关闭,释放资源
// 创建一个 BufferedReader 对象,用于从文件中读取数据
BufferedReader br = new BufferedReader(new FileReader("data.txt"));

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

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

使用 BufferedWriter 写入文件的示例:

  1. 创建了一个 BufferedWriter 对象,用于将数据写入到文件中
  2. 使用 write() 方法将数据写入到缓存区中(和使用 FileWriter 类似)
  3. 通过 flush() 方法将缓存区中的数据写入到文件中
  4. close() 方法关闭 BufferedWriter,释放资源
// 创建一个 BufferedWriter 对象,用于将数据写入到文件中
BufferedWriter bw = new BufferedWriter(new FileWriter("data.txt"));

// 将数据写入到文件中
String data = "输入输出";
bw.write(data);

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

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

缓冲

优点:可以提高读写效率,减少了频繁的读写磁盘或网络的次数,从而提高了程序的性能

缺点:需要注意缓冲区的大小和清空缓冲区的时机,以避免数据丢失或不完整的问题

6)打印 

打印输出数据的类 包括 PrintStream 和 PrintWriter 两个类

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 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程

  1. 创建一个 ByteArrayOutputStream 对象 buffer,用于存储数据
  2. try-with-resources 创建一个 ObjectOutputStream 对象 output,并将其与 buffer 关联
  3. 使用 writeUTF() 方法将字符串写入到缓冲区中
  4. try-with-resources 语句执行完毕时,会自动调用 output 的 close() 方法关闭输出流,释放资源
  5. 使用 toByteArray() 方法将缓冲区中的数据转换成字节数组
  6. 使用 Arrays.toString() 方法将字节数组转换成字符串,并输出到控制台
// 创建一个 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();
}

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();

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();

在进行文本文件读写时,通常使用字符流进行操作,而在进行网络传输或与设备进行通信时,通常使用字节流进行操作

 注:在使用转换流时需要注意字符编码的问题。如果不指定字符编码,则使用默认的字符编码,可能会出现乱码问题


2. 文件流 Java File

File 跟流无关,File 类不能对文件进行读和写

2.1 File 构造

// 文件路径名
String path = "/Users/username/123.txt";
File file1 = new File(path); -------相当于/Users/username/123.txt
// 文件路径名
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

注:

  1. macOS 与 Linux 路径使用正斜杠 / 作为路径分隔符,而 Windows 路径使用反斜杠作 \ 为路径分隔符,使用 File.separator ,这个属性会根据操作系统自动返回正确的路径分隔符
  2. File 类的构造方法不会检验这个文件或目录是否真实存在,因此无论该路径下是否存在文件或者目录,都不影响 File 对象的创建

2.2 常用方法

1)获取功能的方法

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 对象表示目录的时候毫无意义

2)绝对路径与相对路径

绝对路径是从文件系统的根目录开始的完整路径

相对路径是相对于当前工作目录的路径

注:Windows 文件系统默认是不区分大小写,Unix 系统中默认是区分大小写的

3)判断功能的方法

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("不是文件");
}

4)创建,删除方法

  • createNewFile():文件不存在,创建一个新的空文件并返回 true,文件存在则返回 false
  • delete():删除文件或目录。如果是目录,只有目录为空才能删除
  • mkdir():只能创建一级目录,如果父目录不存在,则创建失败
  • 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());
}

5)目录遍历

  • String[ ] list():返回一个 String 数组,表示该 File 目录中的所有子文件或目录
  • File[ ] listFiles():返回一个 File 数组,表示该 File 目录中的所有的子文件或目录
File directory = new File("/Users/Documents/Github");

// 列出目录下的文件名
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. 指定的目录必须存在
  2. 指定必须是目录,否则引发 NullPointException 异常

6)递归遍历 

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

    // 递归遍历目录下的文件和子目录
    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.2 RandomAccessFile

RandomAccessFile 既可以用来读取文件,也可以用来写入文件

不同之处:允许跳转到文件的任何位置,从那里开始读取或写入,特别适用于需要在文件中随机访问数据的场景,如数据库系统

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

public class RandomAccessFileDemo {

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

        try {
            // 使用 RandomAccessFile 写入文件
            writeToFile(filePath, "Hello, world!");

            // 使用 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 编码处理字符串

1)构造方法

  • RandomAccessFile(File file, String mode):使用给定的文件对象和访问模式
  • RandomAccessFile(String name, String mode):使用给定的文件名和访问模式

访问模式 mode 的值可以是:

  • "r":以只读模式打开文件,使用了写操作会导致 IOExpection
  • "rw":以读写模式打开文件。如果文件不存在,它将被创建
  • "rws":以读写模式打开文件,并要求对内容或元数据的每个更新都被立即写入到底层存储设备。这种模式是同步的,可以确保在系统崩溃时不会丢失数据
  • "rwd":与“rws”类似,以读写模式打开文件,但仅要求对文件内容的更新被立即写入。元数据可能会被延迟写入

2)主要方法

  • 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 编码的字符串
  • 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 编码写入文件

2.3 Apache FileUtils 类

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

1)复制文件或者目录

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

2)删除文件或者目录

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

2.4 Hutool FileUtil 类

1)复制文件

将指定的源文件复制到指定的目标文件中

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

2)移动文件或目录

FileUtil.move(file, dest, true);

3)删除文件或目录

FileUtil.del(file);

4)重命名

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

5)读取每一行数据

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

3. 字节流

一切文件(文本、视频、图片)的数据都是以二进制的形式存储的,传输时也是。所以,字节流可以传输任意类型的文件数据

3.1 字节输出流

OutputStream字节输出流超类(父类),它定义的一些共性方法:

  • close():关闭此输出流并释放与此流相关联的系统资源
  • flush():刷新此输出流并强制缓冲区的字节被写入到目的地
  • write(byte[] b):将 b.length 个字节从指定的字节数组写入此输出流
  • write(byte[ ] b, int off, int len):从指定的字节数组写入 len 字节到此输出流,从偏移量 off开始。 也就是说从off个字节数开始一直到len个字节结束

FileOutputStream类,用于将数据写入到文件

1)FileOutputStream 构造方法

1. 使用文件名创建:

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

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

2. 使用文件对象创建

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

使用示例:

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 文件中,最后关闭了输出流

2)FileOutputStream 写入字节数据

1. 写入字节

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

注:一个字节只有8位,因此参数 b 的取值范围应该在 0 到 255 之间,超出这个范围的值将会被截断;当将一个整型值传递给 write(int b) 方法时,方法会将该值转换为 byte 类型,发生截断只会将参数 b 的低8位写入,而忽略高24位

2. 写入字节数组

// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");     
// 字符串转换为字节数组
byte[] b = "在深度学习里迷路".getBytes();
// 写入字节数组数据
fos.write(b);
// 关闭资源
fos.close();

3. 写入指定长度字节数组

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

3)实现数据追加、换行

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

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

如果文件不存在,则创建一个新文件;如果文件已经存在,则在文件末尾追加数据。

true 表示追加数据,false 表示不追加也就是清空原有数据

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

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

如果文件不存在,则创建一个新文件;如果文件已经存在,则在文件末尾追加数据

3. Windows 中,换行符号 \r\n

String filename = "example.txt";
FileOutputStream fos = new FileOutputStream(filename, true);  // 追加模式
String content = "你好呀\r\n";  // 使用回车符和换行符的组合
fos.write(content.getBytes());
fos.close();

4. macOS 中,换行符 \n

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

3.2 字节输入流

InputStream 是字节输入流超类(父类),它的一些共性方法:

  • close():关闭此输入流并释放与此流相关的系统资源
  • int read():从输入流读取数据的下一个字节
  • read(byte[ ] b):该方法返回的 int 值代表的是读取了多少个字节,读到几个返回几个,读取不到返回-1

FileInputStream 就是一个子类,是文件输入流,用于将数据从文件中读取数据

1)构造方法

1. FileInputStream(String name)

创建一个 FileInputStream 对象,并打开指定名称的文件进行读取。如果文件不存在,将会抛出 FileNotFoundException 异常

2. FileInputStream(File file)

创建一个 FileInputStream 对象,并打开指定的 File 对象表示的文件进行读取

2)读取字节数据

1. 读取字节

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

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

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

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

2. 字节数组读取

从输入流中最多读取 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();

3)复制文件

原理:将文件读入到字节输入流,然后通过字节输出流写入文件

// 创建一个 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();

4. 字符流

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

注: 字符流 = 字节流 + 编码表(要注意处理乱码问题)

4.1 字符输入流

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

  • close():关闭此流并释放与此流相关的系统资源
  • read():从输入流读取一个字符
  • read(char[ ] cbuf):从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中

FileReader 是 Reader 的子类,用于从文件中读取字符数据

1)构造方法

  • FileReader(File file):创建一个新的 FileReader,参数为File对象
  • FileReader(String fileName):创建一个新的 FileReader,参数为文件名
// 使用File对象创建流对象
File file = new File("a.txt");
FileReader fr = new FileReader(file);

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

2)读取数据

1. 读取字符

每次可以读取一个字符,返回读取的字符(转为 int 类型),当读取到文件末尾时,返回 -1

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

2. 读取指定长度的字符

每次读取 len 个字符,然后使用 String 构造方法将其转换为字符串并输出

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));
    }
}

4.2 字符输出流

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

  • write(int c):写入单个字符
  • write(char[ ] cbuf):写入字符数组
  • write(char[ ] cbuf, int off, int len):写入字符数组的一部分,off为开始索引,len为字符个数
  • write(String str):写入字符串
  • write(String str, int off, int len):写入字符串的某一部分
  • flush():刷新该流的缓冲
  • close():关闭此流,但要先刷新它

FileWrite 是 Writer 子类,用来将字符写入到文件

1)构造方法

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

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

2)写入数据

1. 写入字符,每次可以写出一个字符

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();
    }
}

注:

1. writer(int b) 写入的是一个字节,而不是一个字符

2. writer(char cbuf[ ])  writer(String str) 写入字符

 2. 写入字符数组

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();
    }
}

3. 写入指定字符数组

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

4. 写入字符串

fw = new FileWriter("output.txt");
String str = "你好呀,哈哈哈";
fw.write(str); // 将字符串写入文件

5. 写入指定字符串

String str = "这个时间变得好快呀!";
try (FileWriter fw = new FileWriter("output.txt")) {
    fw.write(str, 0, 5); // 将字符串的前 5 个字符写入文件
} catch (IOException e) {
    e.printStackTrace();
}

3)关闭、刷新

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

关闭了流对象,就无法继续写数据了。如果既想写入数据,又想继续使用流,就需要 flush()、

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

 4)续写与换行

// 使用文件名称创建流对象,可以续写数据
FileWriter fw = new FileWriter("fw.txt",true);     
// 写出字符串
fw.write("你好");
// 写出换行
fw.write("\r\n");
// 写出字符串
fw.write("世界");
// 关闭资源
fw.close();

5)文本文件的复制

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();
    }
}

5. 缓冲流

工作原理:将数据先写入缓冲区中,当缓冲区满时再一次性写入文件或输出流,或者当缓冲区为空时一次性从文件或输入流中读取一定量的数据。

目的:减少系统的 I/O 操作次数,提高系统的 I/O 效率,从而提高程序的运行效率

5.1 字节缓冲流

BufferedInputStream 和 BufferedOutputStream 属于字节缓冲流

1)构造方法

// 创建字节缓冲输入流,先声明字节流
FileInputStream fps = new FileInputStream(b.txt);
BufferedInputStream bis = new BufferedInputStream(fps)

// 创建字节缓冲输入流(一步到位)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("b.txt"));

// 创建字节缓冲输出流(一步到位)
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt"));

2)缓冲流解决的问题

思想:一次多读点多写点,减少读写的频率,用空间换时间

BufferedInputStream 的 read 方法:

public synchronized int read() throws IOException {
    if (pos >= count) {     // 如果当前位置已经到达缓冲区末尾
        fill();             // 填充缓冲区
        if (pos >= count)   // 如果填充后仍然到达缓冲区末尾,说明已经读取完毕
            return -1;      // 返回 -1 表示已经读取完毕
    }
    return getBufIfOpen()[pos++] & 0xff; // 返回当前位置的字节,并将位置加 1
}

FileInputStream 的 read 方法:

调用的是系统底层的方法 read0() ,不同系统上它们的功能都是相同的,都是用于读取一个字节

BufferedOutputStream 的 write 方法:

public synchronized void write(byte b[], int off, int len) throws IOException {
    if (len >= buf.length) {    // 如果写入的字节数大于等于缓冲区长度
        /* 如果请求的长度超过了输出缓冲区的大小,
           先刷新缓冲区,然后直接将数据写入。
           这样可以避免缓冲流级联时的问题。*/
        flushBuffer();          // 先刷新缓冲区
        out.write(b, off, len); // 直接将数据写入输出流
        return;
    }
    if (len > buf.length - count) { // 如果写入的字节数大于空余空间
        flushBuffer();              // 先刷新缓冲区
    }
    System.arraycopy(b, off, buf, count, len); // 将数据拷贝到缓冲区中
    count += len;                             // 更新计数器
}

思想:

  1. 检查写入的字节数是否大于等于缓冲区长度
  2. 如果是,则先将缓冲区中的数据刷新到磁盘中,然后直接将数据写入输出流
  3. 如果写入的字节数小于缓冲区长度,则检查缓冲区中剩余的空间是否足够容纳要写入的字节数,如果不够,则先将缓冲区中的数据刷新到磁盘中
  4. 使用 System.arraycopy() 方法将要写入的数据拷贝到缓冲区中,并更新计数器 count
  5. 如果写入的字节数小于缓冲区长度且缓冲区中还有剩余空间,则直接将要写入的数据拷贝到缓冲区中,并更新计数器 count

步骤 1 的目的:避免缓冲流级联时的问题 -> 缓冲区的大小不足以容纳写入的数据时,可能会引发级联刷新,导致效率降低

级联问题(Cascade Problem)是指在一组缓冲流(Buffered Stream)中,由于缓冲区的大小不足以容纳要写入的数据,导致数据被分割成多个部分,并分别写入到不同的缓冲区中,最终需要逐个刷新缓冲区,从而导致性能下降的问题。

 也就是说,只有当 buf 写满了,才会 flush,将数据刷到磁盘,默认一次刷 8192 个字节

public BufferedOutputStream(OutputStream out) {
    this(out, 8192);
}

如果 buf 没有写满,会继续写 buf

3)byte & 0xFF

byte 类型是有符号的,即其取值范围为 -128 到 127。如果我们希望得到的是一个无符号的 byte 值,就需要使用 byte & 0xFF 来进行转换

 5.2 字符缓冲流

BufferedReader 类继承自 Reader 类,readLine() 方法可以一次读取一行数据,而不是一个字符一个字符地读取

BufferedWriter 类继承自 Writer 类,newLine() 方法可以写入一个系统特定的行分隔符

1)构造方法

// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("b.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));

2)特有方法

  • BufferedReader 类:readLine() 方法可以一次读取一行数据
  • BufferedWriter 类:newLine() 方法可以换行
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
// 定义字符串,保存读取的一行文字
String line  = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
    System.out.print(line);
    System.out.println("------");
}
// 释放资源
br.close();
// 创建流对象
BfferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));
// 写出数据
bw.write("你");
// 写出换行
bw.newLine();
bw.write("好");
bw.newLine();
bw.write("呀");
bw.newLine();
// 释放资源
bw.close();

6. 转换流

转换流主要有两种类型:InputStreamReader 和 OutputStreamWriter

6.1 编码与解码

在计算机中,数据通常以二进制形式存储和传输

  • 编码就是将原始数据(比如说文本、图像、视频、音频等)转换为二进制形式
  • 解码就是将二进制数据转换为原始数据,是一个反向的过程
String str = "你好";
String charsetName = "UTF-8";

// 编码
byte[] bytes = str.getBytes(Charset.forName(charsetName));
System.out.println("编码: " + bytes);

// 解码
String decodedStr = new String(bytes, Charset.forName(charsetName));
System.out.println("解码: " + decodedStr);

6.2 字符集

Charset:字符集,是一组字符的集合,每个字符都有一个唯一的编码值,也称为码点

 6.3 InputStreamReader

作用是将字节流(InputStream)转换为字符流(Reader),同时支持指定的字符集编码方式,从而实现字符流与字节流之间的转换

1)构造方法

// 创建一个使用默认字符集的字符流
InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
// 创建一个指定字符集的字符流
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");

2)解决编码

String s = "你好呀!";

try {
    // 将字符串按GBK编码方式保存到文件中
    OutputStreamWriter outUtf8 = new OutputStreamWriter(
            new FileOutputStream("logs/test_utf8.txt"), "GBK");
    outUtf8.write(s);
    outUtf8.close();

    // 将字节流转换为字符流,使用GBK编码方式
    InputStreamReader isr = new InputStreamReader(new         
        FileInputStream("logs/test_utf8.txt"), "GBK");
    // 读取字符流
    int c;
    while ((c = isr.read()) != -1) {
        System.out.print((char) c);
    }
    isr.close();
} catch (IOException e) {
    e.printStackTrace();
}

6.4 OutputStreamWriter

将字符流转换为字节流,是字符流到字节流的桥梁

1)构造方法

// 创建一个使用默认字符集的字符流
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("a.txt"));
// 创建一个指定字符集的字符流
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("b.txt") , "GBK");

2)使用

通常为了提高读写效率,我们会在转换流上再加一层 缓冲流

try {
    // 从文件读取字节流,使用UTF-8编码方式
    FileInputStream fis = new FileInputStream("test.txt");
    // 将字节流转换为字符流,使用UTF-8编码方式
    InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
    // 使用缓冲流包装字符流,提高读取效率
    BufferedReader br = new BufferedReader(isr);
    // 创建输出流,使用UTF-8编码方式
    FileOutputStream fos = new FileOutputStream("output.txt");
    // 将输出流包装为转换流,使用UTF-8编码方式
    OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
    // 使用缓冲流包装转换流,提高写入效率
    BufferedWriter bw = new BufferedWriter(osw);

    // 读取输入文件的每一行,写入到输出文件中
    String line;
    while ((line = br.readLine()) != null) {
        bw.write(line);
        bw.newLine(); // 每行结束后写入一个换行符
    }

    // 关闭流
    br.close();
    bw.close();
} catch (IOException e) {
    e.printStackTrace();
}

7. 序列流

一种可以将 Java 对象序列化和反序列化的流

在 Java 中,序列化通过实现 java.io.Serializable 接口来实现,只有实现了 Serializable 接口的对象才能被序列化

反序列化是指将一个字节序列转换为一个对象,以便在程序中使用

7.1 ObjectOutputStream

java.io.ObjectOutputStream 继承自OutputStream 类,因此可以将序列化后的字节序列写入到文件、网络等输出流中

1)构造方法

构造方法接收一个 OutputStream 对象作为参数,将序列化后的字节序列输出到指定的输出流中

FileOutputStream fos = new FileOutputStream("file.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

一个对象要想序列化,必须满足两个条件:

  1. 实现 java.io.Serializable 接口
  2. 该类的所有字段都必须是可序列化的。如果一个字段不需要序列化,则需要使用 transient 关键字进行修饰
public class Employee implements Serializable {
    public String name;
    public String address;
    public transient int age; // transient瞬态修饰成员,不会被序列化
}

2)使用

writeObject 方法,将对象序列化成字节序列并输出到输出流中的方法,可以处理对象之间的引用关系、继承关系、静态字段和 transient 字段

public class ObjectOutputStreamDemo {
    public static void main(String[] args) {
        Person person = new Person("小林", 20);
        try {
            FileOutputStream fos = new FileOutputStream("logs/person.dat");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(person);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
class Person implements 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;
    }
}

7.2 ObjectInputStream

ObjectInputStream 可以读取 ObjectOutputStream 写入的字节流,并将其反序列化为相应的对象

1)构造及使用

String filename = "logs/person.dat"; // 待反序列化的文件名
try (FileInputStream fileIn = new FileInputStream(filename);
     ObjectInputStream in = new ObjectInputStream(fileIn)) {
     // 从指定的文件输入流中读取对象并反序列化
     Object obj = in.readObject();
     // 将反序列化后的对象强制转换为指定类型
     Person p = (Person) obj;
     // 打印反序列化后的对象信息
     System.out.println("Deserialized Object: " + p);
} catch (IOException | ClassNotFoundException e) {
     e.printStackTrace();
}

7.3 Kryo

实际开发中,很少使用 JDK 自带的序列化和反序列化

  1. 可移植性差:Java 特有的,无法跨语言进行序列化和反序列化
  2. 性能差:序列化后的字节体积大,增加了传输/保存成本
  3. 安全问题:应用安全:JAVA反序列化漏洞之殇

Kryo 是一个优秀的 Java 序列化和反序列化库,具有高性能、高效率和易于使用和扩展等特点

地址:https://github.com/EsotericSoftware/kryo

 使用:

1. 在 pom.xml 中引入依赖

<!-- 引入 Kryo 序列化工具 -->
<dependency>
     <groupId>com.esotericsoftware</groupId>
     <artifactId>kryo</artifactId>
     <version>5.4.0</version>
</dependency>

2. 调用

public class KryoDemo {
    public static void main(String[] args) throws FileNotFoundException {
        Kryo kryo = new Kryo();
        // 将对象进行注册
        kryo.register(KryoParam.class);

        KryoParam object = new KryoParam("小林", 123);

        Output output = new Output(new FileOutputStream("logs/kryo.bin"));
        kryo.writeObject(output, object);
        output.close();

        Input input = new Input(new FileInputStream("logs/kryo.bin"));
        KryoParam object2 = kryo.readObject(input, KryoParam.class);
        System.out.println(object2);
        input.close();
    }
}

class KryoParam {
    private String name;
    private int age;

    public KryoParam() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "KryoParam{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

8. 打印流

PrintStream 是 OutputStream 的子类(字节流),PrintWriter 是 Writer 的子类(字符流)

特点:

  • 可以自动进行数据类型转换:打印流可以将各种数据类型转换为字符串,并输出到指定的输出流中。
  • 可以自动进行换行操作:打印流可以在输出字符串的末尾自动添加换行符,方便输出多个字符串时的格式控制。
  • 可以输出到控制台或者文件中:打印流可以将数据输出到控制台或者文件中,方便调试和日志记录

 printf 方法

常用转换说明符及对应的输出格式:

  • %s:输出一个字符串。
  • %d 或 %i:输出一个十进制整数。
  • %x 或 %X:输出一个十六进制整数,%x 输出小写字母,%X 输出大写字母。
  • %f 或 %F:输出一个浮点数。
  • %e 或 %E:输出一个科学计数法表示的浮点数,%e 输出小写字母 e,%E 输出大写字母 E。
  • %g 或 %G:输出一个浮点数,自动选择 %f 或 %e/%E 格式输出。
  • %c:输出一个字符。
  • %b:输出一个布尔值。
  • %h:输出一个哈希码(16进制)。
  • %n:换行符。

还支持一些修饰符,用于指定输出的宽度、精度、对齐方式等

  • 宽度修饰符:用数字指定输出的最小宽度,如果输出的数据不足指定宽度,则在左侧或右侧填充空格或零。
  • 精度修饰符:用点号(.)和数字指定浮点数或字符串的精度,对于浮点数,指定小数点后的位数,对于字符串,指定输出的字符数。
  • 对齐修饰符:用减号(-)或零号(0)指定输出的对齐方式,减号表示左对齐,零号表示右对齐并填充零。
int num = 123;
System.out.printf("%5d\n", num); // 输出 "  123"
System.out.printf("%-5d\n", num); // 输出 "123  "
System.out.printf("%05d\n", num); // 输出 "00123"

double pi = Math.PI;
System.out.printf("%10.2f\n", pi); // 输出 "      3.14"
System.out.printf("%-10.4f\n", pi); // 输出 "3.1416    "

String name = "是打印流";
System.out.printf("%10s\n", name); // 输出 "     是打印流"
System.out.printf("%-10s\n", name); // 输出 "是打印流     "

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值