当前位置:首页 » 《随便一记》 » 正文

【Java】文件类 和 I/O流详解

19 人参与  2024年02月25日 08:51  分类 : 《随便一记》  评论

点击全文阅读


文章目录

一. 文件概述二. 文件类File1. 构造方法和常用普通方法2. 4种获取路径方法的比较 三. I/O流1. 流的概念2. FileReader和FileWriter3. FileInputStream和FileOutputStream4. 带有缓冲功能的I/O流(处理流)关闭流资源的另一种方法(推荐)5. 对象流—ObjectInputStream 和 ObjectOutputStream(处理流)6. 转换流—InputStreamReader 和 OutputStreamWriter(处理流)7. System.in 和 System.out8. Scanner 和 PrintWriter、PrintStream

?前言:
本文是对个人学习过程中文件操作、常见I/O流的类型和使用的总结。

一. 文件概述

什么是文件?

文件是计算机中存储数据的一种方式,它可以用来保存各种类型的信息,如文本、图像、音频和视频等。文件通常由文件名和扩展名组成,扩展名(后缀名)表示文件的类型或格式。

在Windows系统中不同的文件后缀通常代表着不同的文件类型和默认打开方式,如 docx为WPS中word文档的后缀,jpg或png为常见的图片文件后缀,exe通过代表一个可执行程序等;但是Unix、Linux等操作系统上则没有这样精确的分类。

文件的分类:
文件总体上可以分为 文本文件(ASCII码文件)二进制文件。文本文件以字符为基本单位,最常见的文本文件为 txt文件;二进制文件由0、1组成,以字节为基本单位,常见的二进制文件有图片、视频、音频等。

文件路径:
在文件系统中,通过文件路径可以定位到一个唯一的文件,文件路径可以分为绝对路径和相对路径。
绝对路径指从根目录开始到目标文件的完整路径。在Windows系统中文件的根目录可能是C:\,在Linux系统中根目录为/。
相对路径指从当前工作目录到目标文件的路径。在相对路径中通常用 . 表示当前目录,用 .. 表示上一级目录。(注意:在IDEA的Java项目中,如果项目的名称为Test,其绝对路径为D:/javacode/Test,则在创建 File对象时,工作目录在Test文件夹下,即D:/javacode/Test 或 D:/javacode/Test/.)

文件分隔符:
在一个文件路径中,不同的目录之间或目录和文件间用文件分隔符隔开。在Windows系统中,可以用 / 或 \\ 作为文件分隔符;在Linux或Unix操作系统中以 / 作为文件分隔符。


二. 文件类File

1. 构造方法和常用普通方法

在Java程序中,文件用 File类 来表示。File类的构造方法和常用方法如下:
在这里插入图片描述
注意:文件路径可使用绝对路径或相对路径。

在这里插入图片描述

2. 4种获取路径方法的比较

代码如下(注意:当前项目工作路径为:D:/javacode/Test)

public static void main(String[] args) throws IOException {    File file = new File("../hello.txt");    System.out.println("文件是否存在: " + file.exists());                // 判断文件是否存在    System.out.println("文件名称: " + file.getName());                  // 获取文件名称    System.out.println("父目录的路径: " + file.getParent());            // 获取父目录的路径    System.out.println("文件路径: " + file.getPath());                 // 获取文件路径    System.out.println("文件的绝对路径: " + file.getAbsolutePath());    // 获取文件的绝对路径    System.out.println("修饰过的绝对路径: " + file.getCanonicalPath()); // 获取文件修饰过的绝对路径}

程序的运行结果如下:
在这里插入图片描述
可以发现:

文件是否存在与File类对象的构建无关。File 对象父目录的路径与 File 对象创建时写入的路径有关。若使用 File file = new File(“hello.txt”)语句构建对象时,父目录路径为 null。getAbsolutePath()获取文件的绝对路径与构建对象时传入参数有关,它使用传入参数来表示文件绝对路径。getCanonicalPath()会对文件的绝对路径进行简化,可以更加直观的了解到文件所处的位置。

三. I/O流

1. 流的概念

什么是流?

流是个抽象的概念,是对输入/输出设备的抽象,Java程序中,对于数据的输入/输出操作都是以“流”的方式进行。设备可以是文件,网络,内存等。

什么是 I/O流?
I/O 是 Input/Output 的缩写,因此I/O 流即输入流 / 输出流,用于处理数据的传输,如 读/写 文件、网络通讯等。(注意:输入/输出都是站在内存(程序)的视角看待的,数据从外部设备流向内存称为输入,从内存流向其他设备称为输出)

流的分类有哪些?

按操作数据的单位不同可分为:字符流(传输数据以单个字符为基本单位)和字节流(传输数据以1个字节为单位)。所有字符流的顶级抽象父类为: Reader和Writer,字节流为:InputStream和OutputStream。按数据流的流向不同可分为:输入流 和 输出流。所有输入流的顶级抽象父类为:Reader和InputStream,输出流为:Writer和OutputStream。按流的角色不同可分为:节点流(从特定的数据源读写数据,如FileReader、StringReader等)和 处理流/包装流(对已存在的流进行连接和封装,以提供更加强大的数据读写功能,如BufferedReader、BufferedInputStream等)。

注意:所有的流都是一种资源,用完应当关闭,否则会造成文件资源泄露,可能导致文件不能被正常打开!!!

2. FileReader和FileWriter

当我们希望从文本文件数据 读写数据时,可以使用FileReader类 和 FileWriter类。它们的构造方法和常用方法如下:

FileReader类:
在这里插入图片描述

在这里插入图片描述

部分方法示例如下:

public static void main(String[] args) throws IOException {    Reader fileReader = null;    try {        Reader = new FileReader("D:/test.txt");        int data;        // 每次读取一个字符        while ((data = fileReader.read()) != -1) {            System.out.print((char) data);        }        // 使用数组读取字符        char[] cubf = new char[5];        int readLen = 0;        while ((readLen = fileReader.read(cubf)) != -1) {            for (int i = 0; i < readLen; i++) {                System.out.print(cubf[i]);            }        }    } finally {    // 关闭文件资源        if(fileReader != null) {            fileReader.close();        }    }}

两个方法的程序运行结果都如下:
在这里插入图片描述

注意:

读取单个字符时,返回0到65535( 0x00-0xffff )范围内的整数,即字符的Unicode编码,需要进行类型转换才能得到对应的字符使用数组读取字符时,应取实际读取字符的个数 readLen,否则会错误使用之前的读取数据(新读取的数据会覆盖原来数组内容)流使用完应该关闭

============这是分隔线

FileWriter类:
在这里插入图片描述

在这里插入图片描述

部分方法示例如下:

public static void main(String[] args) throws IOException {    FileWriter fileWriter = null;    try {        Writer = new FileWriter("D:/test.txt");  // 以覆盖的方式写入数据        // 1. 写入单个字符        fileWriter.write('a');        // 2. 写入一个字符串        fileWriter.write(" hello world");        // 3. 写入一个字符数组的一部分        char[] arr = new char[]{' ', ' ', 'a', 'b', 'c'};        fileWriter.write(arr, 1, 3);    } finally {        // 关闭文件资源        if(fileWriter != null) {            fileWriter.close();        }    }}

程序运行后文件内容如下:
在这里插入图片描述

注意:

使用FileWriter写入数据时,数据会被立即写入文件。FileWriter没有显式的缓存机制,但在操作系统层面存在一定程度的缓冲机制,因此调用write()方法并不是每次都立即将数据写入文件,为确保数据被及时写入文件,应调用close()方法关闭流达到这一效果。FileWriter调用 flush() 方法实际上使用的是Writer的空实现方法,并不起刷新内存的作用。使用完流对象后应当使用 close()方法关闭资源。

3. FileInputStream和FileOutputStream

当我们需要传输视频、音频、图片等二进制文件时,必须使用字节流读写文件。如果使用字符流读取,文件数据很可能会丢失,造成文件的损坏,导致二进制文件不能被正常使用。

FileInputStream 和 FileOutputStream的构造方法和常用方法如下:
在这里插入图片描述

在这里插入图片描述

使用字节流读文本文件示例如下:

public static void main(String[] args) throws IOException {    InputStream fileInputStream = null;    try {        fileInputStream = new FileInputStream("d:/test.txt");        int data = -1;        while((data = fileInputStream.read()) != -1) {            System.out.printf("%x ", data);        }    } finally {        if(fileInputStream != null) {            fileInputStream.close();        }    }}

文本文件内容、程序运行结果及文件内容UTF8编码如下:
在这里插入图片描述

============这是分隔线

在这里插入图片描述

在这里插入图片描述

方法使用示例如下:(读取图片文件数据,将数据写入到另一个文件,完成图片的复制)

public static void main(String[] args) throws IOException {    OutputStream fileOutputStream = null;    InputStream fileInputStream = null;    try {        fileInputStream = new FileInputStream("d:/100.jpg");        fileOutputStream = new FileOutputStream("d:/temp/copy.jpg");        byte[] cubf = new byte[1024];        int readLen = 0;        while((readLen = fileInputStream.read(cubf)) != -1) {            fileOutputStream.write(cubf, 0, readLen);        }    } finally {        if(fileInputStream != null) fileInputStream.close();        if(fileOutputStream != null) fileOutputStream.close();    }}

程序运行结果如下:
在这里插入图片描述
注意:

使用FileOutputStream写入数据时,数据会被立即写入文件。FileOutputStream没有显式的缓存机制,但在操作系统层面存在一定程度的缓冲机制,因此调用write()方法并不是每次都立即将数据写入文件,为确保数据被及时写入文件,应调用close()方法关闭流达到这一效果。FileOutputStream调用 flush() 方法实际上使用的是OutputStream的空实现方法,并不起刷新内存的作用。使用完流对象后应当使用 close()方法关闭资源。

4. 带有缓冲功能的I/O流(处理流)

缓冲区是什么?
缓冲区相当于一个蓄水池,当降水较少,我们可以从蓄水池中取水,避免了因水压低而取水较慢的问题;当降雨量较多,水压处于正常水平时,我们可以往蓄水池中注水,以备下次使用。(例子可能不太恰当,但缓存区可以简单理解为能够存储数据的蓄水池)

使用带缓冲的处理流的好处:

减少实际的物理读/写次数,提高性能:当从输入流读取数据时,可以一次性读取多个字节或字符到内存的缓冲区中,后面每次都从缓存区读取数据,当缓冲区为空后才会重新从数据源读取数据;当往输出流写入数据时,会先把数据写入缓冲区,直到缓冲区为满时再将数据一次性写入输出设备。由于内存比硬盘等其他设备读写效率更高,因此减少I/O次数可以减轻系统开销,提高程序性能。支持按行读/写数据:字符流可以使用 readLine() 方法方便地按行读取数据,使用 newLine() 方法方便地写入换行符。支持标记和重置:一些输入流可以在读取过程中标记当前位置,然后在需要时重新回到该位置。

带有缓存作用可以分为字符流和字节流,且通常以Buffered开头:(以下都为实现类)
字节流:BufferedInputStreamBufferedOutputStream
字符流:BufferedReaderBufferedWriter

以上缓冲I/O流的类图继承关系大致如下:
在这里插入图片描述
在这里插入图片描述
可以发现它们都继承自身的顶级父类,其构造方法能够接受所有实现子类

字符缓冲流和字节缓冲流的主要方法与之前的方法介绍几乎一致,但字符缓冲流可以使用 readLine() 方法方便地按行读取数据,使用 newLine() 方法可以方便地写入换行符;且所有的缓冲输入流都支持mark() 和 reset()方法。(具体的方法介绍和使用可以参照 io包下:Java8 API文档)

注意:带缓冲的输出流可以手动调用 flush() 方法刷新缓冲区,让缓冲区中的数据立即写入输出设备。

关闭流资源的另一种方法(推荐)

由于每次创建流对象都会占用文件描述符表等系统资源,如果长期使用完后 没有关闭或忘记关闭,可能导致文件描述表被占满,后续文件不能被正常打开。
因此,我们可以在每次构建流对象时用 try() 语句将 Java语句包裹起来,这样的话我们就可省去了手动调用 close() 方法的步骤,因为当 try 包裹起来的所有语句执行结束后, close()方法会被自动调用。

具体的使用示例如下:

public static void main(String[] args) throws IOException {    // 1. 将先创建的节点流作为参数传递给处理流,先关闭外层流,后关闭内层流即节点流    try(FileInputStream fileInputStream = new FileInputStream("d:/test.txt")) {        try (BufferedInputStream bufferedReader = new BufferedInputStream(fileInputStream)) {            // 具体的代码        }    }// 2. 直接创建处理流,关闭最外层流即可,内层流也会被关闭    try(BufferedReader bufferedReader = new BufferedReader(new FileReader("d:/test.txt"))) {        // 具体的代码    }}

注意:

只有实现的 Closeable接口的类才可以使用此方式关闭。当使用多个流进行“装饰”时,只需关闭最外层的流对象即可,在JDK源码中内层流也会被关闭。

5. 对象流—ObjectInputStream 和 ObjectOutputStream(处理流)

在某些情况下我们想保存的可能不是文本信息、音频、图片等数据,而是保存数据的值及类型或一个Java对象的信息时,应该使用ObjectInputStream 和 ObjectOutputStream对数据进行读/写。
其中写入数据时,保存数据的值和类型称为序列化;读取数据时,恢复数据的值和类型称为反序列化

为什么需要进行序列化和反序列化?
在这里插入图片描述
在这里插入图片描述
可以发现:只有保存了数据的值和类型,才能保证数据在读取时被正确解析成原来的数据

能够被序列化和反序列化需要满足以下条件之一:

类实现Serializable接口(推荐,该接口为标记接口,无需重写方法)类实现Externalizable接口(不推荐,实现该接口需要重写里面的方法)

===================
ObjectOutputStream 和 ObjectInputStream的继承关系图如下:
在这里插入图片描述

ObjectOutputStream 和 ObjectInputStream 的构造方法和常用普通方法(只列出ObjectOutStream,ObjectInputStream同理)如下:
在这里插入图片描述

在这里插入图片描述

方法使用示例如下:

public static void main(String[] args) throws IOException, ClassNotFoundException {    try(ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("d:/data.dat"))) {        objectOutputStream.writeByte(100);        objectOutputStream.writeInt(10000);        objectOutputStream.writeBoolean(false);        objectOutputStream.writeDouble(3.14);        objectOutputStream.writeUTF("你好 world!");                // 写入一个对象,需实现 Serializable接口,否则会报异常        objectOutputStream.writeObject(new Cat());    }        try(ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("d:/data.dat"))) {        System.out.println(objectInputStream.readByte());        System.out.println(objectInputStream.readInt());        System.out.println(objectInputStream.readBoolean());        System.out.println(objectInputStream.readDouble());        System.out.println(objectInputStream.readUTF());        Cat cat = (Cat) objectInputStream.readObject();        // 打印该对象的类型        System.out.println(cat.getClass());    }}

程序运行结果如下:
在这里插入图片描述
在这里插入图片描述

注意:

序列化/反序列化的顺序必须一致,否则可能导致数据读取出现错误。序列化和反序列化的对象必须都实现 Serializable接口 ,且对象中属性的类型也需要实现序列化接口。序列化对象时,默认将里面所有的属性都进行序列化,除了 static或transient 修饰的成员。为了提高版本的兼容性,序列化的类中建议添加SerialVersionUID。序列化具备可继承性,某类实现了序列化,其所有子类也默认实现了序列化。

6. 转换流—InputStreamReader 和 OutputStreamWriter(处理流)

当从字节流读取字符数据时(如 FileInputStream、Socket.getInputStream()),由于字符编码有很多种,如果没有对所读取的字节流指定所需的字符编码,可能导致数据被错误地解析,引起乱码的现象。(示例如下)

public static void main(String[] args) throws IOException {    try (BufferedReader br = new BufferedReader(new FileReader("d:/test.txt"))) {        System.out.println(br.readLine());    }}

在这里插入图片描述
解决以上乱码问题的方法就是:先用字节流作为输入流,再使用 InputStreamReader 将字节流转换为字符流 并指定字符编码集,这样就能将字符数据进行正确解析。(示例如下,可以先了解下面的内容再回头看解决方法)

public static void main(String[] args) throws IOException {    try (InputStreamReader isr = new InputStreamReader(new FileInputStream("d:/test.txt"), "GBK")) {        try (BufferedReader br = new BufferedReader(isr)) {            System.out.println(br.readLine());        }    }}

在这里插入图片描述

====================
InputStreamReader 和 OutputStreamWriter 的常用构造方法如下:
在这里插入图片描述

通常情况下,转换流只作为中间的“装饰”流,起到指定字符编码的作用,并不作为最终 读/写 数据的输入/输出流,因此其普通方法不用特意了解,只需知道与之前介绍的的字符输入/输出流大致一样即可。

7. System.in 和 System.out

在Java程序中,我们经常会使用到 new Scanner(System.in) 和 System.out.println() 这两种方式来进行输入和输出,其实System.in 和 System.out 也分别代表着一种流。其中

System.in:标准输入流,流的编译类型为InputStream,运行类型为BufferedInputStream,默认设备为键盘。
System.out:标准输出流,流的编译类型为PrintStream,运行类型为PrintStream,默认设备为显示器(控制台)。

若想改变系统默认的输入/输出设备,可以通过System.setIn() 和 System.setOut() 来改变默认配置,其中System.setIn()需要传入InputStream类的子类作为参数,System.setOut()需要传入PrintStream类作为参数。

使用示例如下:(从键盘输入两次,第一次将输入数据打印到控制台,第二次将数据输出到文件)

public static void main(String[] args) throws FileNotFoundException {    // 将内容输出到控制台    Scanner scanner = new Scanner(System.in);    String input1 = scanner.nextLine();    System.out.println(input1);    // 修改输出位置,将内容输出到文件    System.setOut(new PrintStream("d:test.txt"));    String input2 = scanner.nextLine();    System.out.println(input2);}

程序运行结果如下:
在这里插入图片描述
在这里插入图片描述

8. Scanner 和 PrintWriter、PrintStream

当我们需要更加灵活地 读取/写入 数据到 输入/输出设备时,可以使用 Scanner对文件或字节输入流进行“装饰”以提供更加强大的数据读取功能(如nextInt(), next()等),使用 PrintWriter 或 PrintStream 对字符流或字节流进行“装饰”以提供更加强大的文本输出功能(如print()、println()可以输出各种基本类型和字符串的数据)。

其类继承关系图如下:
在这里插入图片描述

PrintWriter 和 PrintStream的区别与联系:

PrintWriter 和 PrintStream都可以输出文本数据,提供了对所有基本数据类型的打印方法(如print()、println() ),都可以在构造方法中指定输出文本数据的字符编码。PrintWriter是 Writer的子类,它用来处理文本数据;PrintStream是 OutputStream的子类,当需要输出二进制数据时,更适合用PrintStream。PrintWriter通过使用checkError()方法来检查是否发生错误;PrintStream在写入过程中则可能抛出IOException异常,需要在代码中进行相应的异常处理。

Scanner 和 PrintWriter、PrintStream详细的构造方法和普通方法可以参考Java8 API文档

Scanner、PrintWriter的使用示例如下:

public static void main(String[] args) throws IOException {    // 使用Scanner读取文件内容    try (Scanner scanner = new Scanner(new File("d:/test.txt"))) {        System.out.println(scanner.nextBoolean());        System.out.println(scanner.nextInt());        System.out.println(scanner.nextLine());        System.out.println(scanner.nextLine());    }    // 使用PrintWriter向文件写入数据,其中字符集为 GBK    try (PrintWriter printWriter = new PrintWriter("d:/f1.txt", "gbk")) {        printWriter.println(true);        printWriter.println(3.1415926);        printWriter.println("hello 世界");    }}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


以上就是本篇文章的全部内容了,如果这篇文章对你有些许帮助,你的点赞、收藏和评论就是对我最大的支持。
另外,文章的不足之处,也希望你可以给我一点小小的建议,我会努力检查并改进。


点击全文阅读


本文链接:http://zhangshiyu.com/post/69826.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1