文章

java重拾-IO

java重拾-IO

img

IO即Input/Output, 程序从外部读入/向外部写出 信息的方式, 常见的外部设备有, 文件(磁盘), 管道(其他进程/线程) 网络(其他设备)

Java 中是通过stream处理IO, stream一个抽象的概念,是指一连串的数据(字符或字节),是以fifo的方式发送信息的通道, 一般是仅读或者仅写的

所有的流可以分为两种, 字符流字节流

字符流是用于传输文本信息, 最小的传输单位为一个字符, 但一个字符占多大嘛, 根据编码来决定, java中一般为reader/writer的类就是字符流, 默认开启了缓冲区

字节流可以用于传输所有的对象信息(图片视频音频...)或其他, 最小传输单位为一个byte, inputStream/outputStream就是字节流, 无默认缓冲区

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():关闭流

img

按操作对象来划分可以分为文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换

6000 字掌握 Java IO 知识体系 | 二哥的Java进阶之路

文件

java.io.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

https://javabetter.cn/io/file-path.html

字节流

OutputStream

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

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

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

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

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

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

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

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

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

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

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

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

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

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

如果不使用追加标志创建对象的话, 将会直接覆盖原文件

InputStream

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

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

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

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

FileInputStream

构造

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

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

读取

使用 FileInputStream 写入字节数据主要通过 read 方法:

read(int b)
read(byte[] b) //从输入流中最多读取 b.length 个字节,并将它们存储到缓冲区数组 b 中
read(byte[] b, int off, int len)
// 读取整个文件
while ((count = fis.read(buffer)) != -1) {
    //sth...
}

字符流

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

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

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

Reader

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

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

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

  • 可以通过构造方法指定要读取的文件路径。
  • 每次可以读取一个或多个字符。
  • 可以读取 Unicode 字符集中的字符,通过指定字符编码来实现字符集的转换。
构造
  • 1、FileReader(File file):创建一个新的 FileReader,参数为File对象
  • 2、FileReader(String fileName):创建一个新的 FileReader,参数为文件名。
读取

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

// 使用文件名称创建流对象
FileReader fr = new FileReader("abc.txt");
// 定义变量,保存数据
int b;
// 循环读取
while ((b = 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-resources 语句自动关闭资源,避免了手动关闭资源的繁琐操作。

Writer

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() 关闭此流,但要先刷新它。

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)
write(char[] cbuf)
write(char[] cbuf, int off, int len)
write(String str)
write(String str, int off, int len)
String str = "沉默王二真的帅啊!";
try (FileWriter fw = new FileWriter("output.txt")) {
    fw.write(str, 0, 5); // 将字符串的前 5 个字符写入文件
} catch (IOException e) {
    e.printStackTrace();
}

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

关闭close和刷新flush

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

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

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

异常处理

// 声明变量
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();
}

缓冲流

缓冲流是对字节流和字符流的一种封装,通过在内存中开辟缓冲区来提高 I/O 操作的效率。Java 通过 BufferedInputStream 和 BufferedOutputStream 来实现字节流的缓冲,通过 BufferedReader 和 BufferedWriter 来实现字符流的缓冲。

缓冲流的工作原理是将数据先写入缓冲区中,当缓冲区满时再一次性写入文件或输出流,或者当缓冲区为空时一次性从文件或输入流中读取一定量的数据。这样可以减少系统的 I/O 操作次数,提高系统的 I/O 效率,从而提高程序的运行效率。

转换流

转换流可以将一个字节流包装成字符流,或者将一个字符流包装成字节流。这种转换通常用于处理文本数据,如读取文本文件或将数据从网络传输到应用程序。

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

InputStreamReader 将一个字节输入流转换为一个字符输入流,而 OutputStreamWriter 将一个字节输出流转换为一个字符输出流。它们使用指定的字符集将字节流和字符流之间进行转换。常用的字符集包括 UTF-8、GBK、ISO-8859-1 等。

二哥的 Java 进阶之路:字节流字符流

InputStreamReader

构造
  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

下面是一个使用 InputStreamReader 解决乱码问题的示例代码:

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

由于使用了 InputStreamReader 对字节流进行了编码方式的转换,因此在读取字符流时就可以正确地解析出中文字符,避免了乱码问题。

OutputStreamWriter

构造
  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName):创建一个指定字符集的字符流。

InputStreamReader 和 OutputStreamWriter 是将字节流转换为字符流或者将字符流转换为字节流。通常用于解决字节流和字符流之间的转换问题,可以将字节流以指定的字符集编码方式转换为字符流,或者将字符流以指定的字符集编码方式转换为字节流。

InputStreamReader 类的常用方法包括:

  • read():从输入流中读取一个字符的数据。
  • read(char[] cbuf, int off, int len):从输入流中读取 len 个字符的数据到指定的字符数组 cbuf 中,从 off 位置开始存放。
  • ready():返回此流是否已准备好读取。
  • close():关闭输入流。

OutputStreamWriter 类的常用方法包括:

  • write(int c):向输出流中写入一个字符的数据。
  • write(char[] cbuf, int off, int len):向输出流中写入指定字符数组 cbuf 中的 len 个字符,从 off 位置开始。
  • flush():将缓冲区的数据写入输出流中。
  • close():关闭输出流。

在使用转换流时,需要指定正确的字符集编码方式,否则可能会导致数据读取或写入出现乱码。

序列流

讲对象与字节流之间转换的流, 可以将 Java 对象序列化和反序列化, 以便在网络上传输或保存到文件中,或者在程序之间传递

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

构造

ObjectOutputStream(OutputStream out)

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

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

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

  • 该类必须实现 java.io.Serializable 接口,否则会抛出 NotSerializableException
  • 该类的所有字段都必须是可序列化的。如果一个字段不需要序列化,则需要使用 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;
    }
}

ObjectInputStream(InputStream in)

创建一个指定 InputStream 的 ObjectInputStream,用于将对象转换并输出到指定的字节流中。

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

实际开发中,很少使用 JDK 自带的序列化和反序列化,这是因为:

  • 可移植性差:Java 特有的,无法跨语言进行序列化和反序列化。
  • 性能差:序列化后的字节体积大,增加了传输/保存成本。
  • 安全问题:攻击者可以通过构造恶意数据来实现远程代码执行,从而对系统造成严重的安全威胁。相关阅读:Java 反序列化漏洞之殇

Kryo 是一个优秀的 Java 序列化和反序列化库,具有高性能、高效率和易于使用和扩展等特点,有效地解决了 JDK 自带的序列化机制的痛点。

打印流

System.out 返回的正是打印流 PrintStream

除此之外,还有它还有一个孪生兄弟,PrintWriter。PrintStream 是 OutputStream 的子类,PrintWriter 是 Writer 的子类,也就是说,一个字节流,一个是字符流

打印流具有以下几个特点:

  • 可以自动进行数据类型转换:打印流可以将各种数据类型转换为字符串,并输出到指定的输出流中。
  • 可以自动进行换行操作:打印流可以在输出字符串的末尾自动添加换行符,方便输出多个字符串时的格式控制。
  • 可以输出到控制台或者文件中:打印流可以将数据输出到控制台或者文件中,方便调试和日志记录(尽管生产环境下更推荐使用 Logback、ELK 等)。

PrintStream 类的常用方法包括:

  • print():输出一个对象的字符串表示形式。
  • println():输出一个对象的字符串表示形式,并在末尾添加一个换行符。
  • printf():使用指定的格式字符串和参数输出格式化的字符串。

printf

来详细说说 printf 方法哈。

public PrintStream printf(String format, Object... args);

其中,format 参数是格式化字符串,args 参数是要输出的参数列表。格式化字符串包含了普通字符和转换说明符。普通字符是指除了转换说明符之外的字符,它们在输出时直接输出。转换说明符是由百分号(%)和一个或多个字符组成的,用于指定输出的格式和数据类型。

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

  • %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:换行符。

除了转换说明符之外,Java 的 printf 方法还支持一些修饰符,用于指定输出的宽度、精度、对齐方式等。

  • 宽度修饰符:用数字指定输出的最小宽度,如果输出的数据不足指定宽度,则在左侧或右侧填充空格或零。
  • 精度修饰符:用点号(.)和数字指定浮点数或字符串的精度,对于浮点数,指定小数点后的位数,对于字符串,指定输出的字符数。
  • 对齐修饰符:用减号(-)或零号(0)指定输出的对齐方式,减号表示左对齐,零号表示右对齐并填充零。

PrintWriter

接下来,我们给出一个 PrintWriter 的示例:

PrintWriter writer = new PrintWriter(new FileWriter("output.txt"));
writer.println("沉默王二");
writer.printf("他的年纪为 %d.\n", 18);
writer.close();

首先,我们创建一个 PrintWriter 对象,它的构造函数接收一个 Writer 对象作为参数。在这里,我们使用 FileWriter 来创建一个输出文件流,并将其作为参数传递给 PrintWriter 的构造函数。然后,我们使用 PrintWriter 的 println 和 printf 方法来输出两行内容,其中 printf 方法可以接收格式化字符串。最后,我们调用 PrintWriter 的 close 方法来关闭输出流。

我们也可以不创建 FileWriter 对象,直接指定文件名。

PrintWriter pw = new PrintWriter("output.txt");
pw.println("沉默王二");
pw.printf("他的年纪为 %d.\n", 18);
pw.close();
License:  CC BY 4.0