【JavaEE】一文学会如何使用:文件IO操作(详解)

目录

前言

什么是IO?

IO流原理

IO流分类

InputStream字节流输入

文件输入流--FileInputStream

理解InputStream中的read方法

关闭文件操作         

利用Scanner进行字符获取

OutputStream字节流输出

文件输出流--FileOutputStream

理解OutputStream中的write方法

利⽤PrintWriter找到我们熟悉的⽅法

Reader读操作

创建Reader对象

FileReader的构造方法

理解Reader类中的不同read方法

使用Reader中的read()方法

利用Scanner进行字符获取

Writer写操作

创建Writer对象

FileWriter构造方法

理解Writer中的write方法

使用Writer中的write方法

运用文件操作和IO实现一个删除指定文件的功能

复制指定文件

在指定目录中查找文件名或文件内容含有关键词的文件


前言

在上一篇中,我们学习了什么是文件,以及如何创建和关闭文件,但那些都是针对文件进行的操作,本篇我们就来讲解如何对文件内容进行操作。

什么是IO?

I/O是“Input/Output”的缩写,中文意思是“输入/输出”,指的是计算机系统与外部环境之间进行的数据交换的过程。我们从键盘输入数据、或者点击鼠标进行操作等这些都是属于输入操作;显示屏、声音这些都是属于输出操作。

当然,不仅局限于硬件设备的IO操作(磁盘IO),还有网络方面的I/O。

IO流原理

IO流是基于流(stream)的概念,将输入的数据和输出的数据看做一个连续的流。数据从一个地方流向另一个地方,流的方向可以是输入(读取数据)或者输出(写入数据)。

IO流的原理是通过流的管道将数据从源头传输到目标地。源头可以是文件、网络连接、内存等,而目标地可以是文件、数据库、网络等。IO流提供了一组丰富的类和方法来实现不同类型的输入和输出操作。

如何判断操作是输入还是输出?我们需要一个参照物,假设以CPU为参照物,那么我们可以认为靠近CPU的操作就是输入,而远离CPU的操作就是输出操作,即如果从CPU上向文件传输数据,那么对于CPU来说,就是在输出,而对于文件来说,这个操作是输入操作。

IO流分类

java中IO流可以按照数据的类型、流的方向和流的功能进行分类。

在Java中,IO流按照数据类型分为两类:

  • 字节流 :读取数据的基本单位是字节,适用于处理二进制数据,如图像、音频、视频等,InputStream/OutputStream
  • 字符流 :读取数据的基本单位是字符,适合处理文本数据。会自动进行字符编码和解码。Reader/Writer

一个字符有多少个字节?

这是不确定的,取决于编码方式,在UTF8中,一个字符是3个字节,而在Unicode中,一个字符是2个字节。

虽然分为两类,但是本质上都是字节流,不过字符流对字节进行了一些封装,根据字符集编码。

按照流的方向分类:

  • 输入流(InputStream):用于读取数据。输入流从数据源读取数据,如文件、网络连接等。常见的输入流类有:FileInputStream、ByteArrayInputStream、SocketInputStream等。
  • 输出流(OutputStream):用于写入数据。将数据写入到目标地,如文件、数据库、网络等。常见的输出流有:FileOutputStream、ByteArrayOnputStream、SocketOnputStream等。

按照流的功能分类

  • 节点流:直接和输入输出源交互。直接与数据源或目的地相连,没有额外的处理逻辑
  • 处理流:间接和输入输出源交互。包装在节点流或其他处理流之上,添加额外的功能。

适用场景

操作场景适用技术
*文件操作读写或写入文件中的数据字节流:使用 FileInputStream 和 FileOutputStream 来读写二进制文件。
字符流:使用 FileReader 和 FileWriter 来读写文本文件。
缓冲流:使用 BufferedInputStream 和 BufferedOutputStream 或 BufferedReader 和 BufferedWriter 来提高文件读写的效率。
转换流:使用 InputStreamReader 和 OutputStreamWriter 来处理不同编码的文本文件。
网络通信在网络上传输数据Socket 编程:使用 Socket 和 ServerSocket 类来实现客户端和服务端之间的通信。
字节流:使用 Socket 的 getInputStream() 和 getOutputStream() 方法来读写数据。
字符流:使用 Socket 的 getInputStream() 和 getOutputStream() 结合 InputStreamReader 和 OutputStreamWriter 来读写文本数据。
内存操作在内存中创建或操作数据流字节流:使用 ByteArrayInputStream 和 ByteArrayOutputStream 来处理内存中的字节数据。
字符流:使用 StringReader 和 StringWriter 来处理内存中的字符数据。
高效数据处理需要高效地处理大量数据缓冲流:使用 BufferedInputStream 和 BufferedOutputStream 或 BufferedReader 和 BufferedWriter 来提高读写速度。
NIO (New I/O):使用 FileChannel 和 ByteBuffer 来实现非阻塞的文件读写,以及内存映射文件等功能。
数据格式化需要以特定格式读写数据字节流:使用 DataInputStream 和 DataOutputStream 来读写基本数据类型。
对象序列化:使用 ObjectInputStream 和 ObjectOutputStream 来读写 Java 对象
异步 I/O需要非阻塞地处理 I/O 操作NIO.2 (Java 7+):使用 AsynchronousFileChannel 和 CompletionHandler 来实现真正的异步 I/O 操作
线程间通信线程之间需要传递数据管道流:使用 PipedInputStream 和 PipedOutputStream 来实现线程间的简单通信。
 
标准输入输出控制台输入输出字节流:使用 System.in 和 System.out 来处理标准输入输出。
字符流:使用 System.in 结合 InputStreamReader 和 System.out 结合 PrintWriter 来处理文本数据。
日志记录记录程序运行时的信息日志框架:使用如 Log4j、SLF4J 等日志框架来记录日志,通常使用字符流进行处理。
图形图像处理处理图像文件字节流:使用 ImageIO 类来读写图像文件,通常需要使用 FileInputStream 和 FileOutputStream。
音频视频处理处理多媒体文件字节流:使用 AudioSystem 和 javax.sound.sampled 包中的类来处理音频文件。
字节流:使用 javax.media 包中的类来处理视频文件。

本篇我们主要讲如何对文件内容进行操作。

InputStream和OutputStream都是抽象类所有字节输入流和输出流的父类

InputStream字节流输入

InputStream是抽象类所有字节输入流父类

在上面中,我们已经知道了InputStream是用来读入数据的。

InputStream常用方法

修饰符及返回值类型方法签名说明
intread()读取一个字节的数据,返回-1代表已经完全读完了
intread(byte[] b)最多读取b.length字节的数据到b中,返回实际读取到的数量;-1代表已经读完了
intread(byte[] b,nt off,int len)最多读取len-off字节的数据到b中,返回实际读取到的数量;-1代表已经读完了
voidclose()关闭字节流

InputStream 只是⼀个抽象类,要使⽤还需要具体的实现类。关于InputStream的实现类有很多,基 本可以认为不同的输⼊设备都可以对应⼀个InputStream类,我们现在只关心从文件中读取,所以使用FileInputStream。 

文件输入流--FileInputStream

构造方法

签名说明
FileInputStream(File file)利用File构造文件输入流
FileInputStream(String name)利用文件路径构造文件输入流

一般常用第二种构造方法,直接写入文本的绝对路径或者相对路径

我们可以来看下:

我们可以看到当在创建一个文件流对象时,是需要抛出一个FileNotFoundException异常的。这是为什么呢? 我们可以看相关注释:如果命名的文件不存在,或者是一个目录或者是由于其他原因无法打开的时候,就会抛出FileNotFoundException异常。

同理的,我们可以看到若是传入一个文件对象,抛出异常的原因和上面基本一样。 

 创建一个输入流对象

        InputStream inputStream=new FileInputStream("./text1.txt");

如果文件不存在就会抛出异常,创建的过程其实就是打开文件的过程。

理解InputStream中的read方法

当我们创建完一个输入流对象之后,那么此时我们就可以调用read方法来读取文件中的数据了。

 我们可以看到有三个重载的read方法。InputStream是字节流,如果想要用数组来接收的话,需要传入一个字节数组。

我们可以看下不带参数的read方法。

这段话的大概意思就是从输入流中读取下一个字节的数据。返回一个取值返回在0~255的int类型的数据。如果读到流的结尾就会返回-1.

示例:假设现在已经有一个文本text.txt,我们往其中输入”你好“

我们可以查看一下“你好”转为16进制是什么,可以看到为“0xe4 0xbd 0xa0 0xe5 0xa5 0xbd”

在线字符串/十六进制互相转换—LZL在线工具 (lzltool.cn)

那么我们期望在从文本读取到的结果也与我们在所查找的一致 

read():每次获取一个字节的数据

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

public class Demo {
    public static void main(String[] args) throws IOException {
        InputStream inputStream=new FileInputStream("./text.txt");
        while(true) {
            int ch=inputStream.read();
            if(ch==-1) break;
            System.out.printf("0x%x ",ch);
        }
    }
}


read(byte[] b):将数据读取到字节数组中
但对于上述这种,每次从硬盘中读取一个字节,所带来的I/O次数太多,效率也不会很高。

所以我们传入一个字节数组去接受每次获取到的字节,尽可能的填满字节数组。

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

public class Demo1 {
   /**
    * 程序的入口点。读取并打印指定文件的二进制内容。
    *
    * @param args 命令行参数
    * @throws IOException 如果文件不存在或无法读取,将抛出此异常
    */
   public static void main(String[] args) throws IOException {
      // 创建一个文件输入流,用于读取指定文件的内容
      InputStream inputStream=new FileInputStream("./text.txt");

      // 创建一个字节数组,用于存储从文件中读取的数据
      byte[] bytes=new byte[1024];

      // 读取文件中的数据到字节数组中,并返回实际读取的字节数
      int ch=inputStream.read(bytes);

      // 遍历已读取的字节数组,并以十六进制格式打印每个字节
      for(int i=0;i<ch;i++){
         System.out.printf("0x%x ",bytes[i]);
      }
   }

}


read(byte[] b,int off,int len):字节数组从off位置开始写入,最大不能超过len。

我们也可以让read从off位置开始向后读入。

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

public class Demo1 {
   /**
    * 程序的入口点。读取并打印指定文件的二进制内容。
    *
    * @param args 命令行参数
    * @throws IOException 如果文件不存在或无法读取,将抛出此异常
    */
   public static void main(String[] args) throws IOException {
      // 创建一个文件输入流,用于读取指定文件的内容
      InputStream inputStream=new FileInputStream("./text.txt");

      // 创建一个字节数组,用于存储从文件中读取的数据
      byte[] bytes=new byte[1024];

      // 读取文件中的数据到字节数组中,并返回实际读取的字节数
      int ch=inputStream.read(bytes,5,100);

      // 遍历已读取的字节数组,并以十六进制格式打印每个字节
      for(int i=5;i<ch+5;i++){
         System.out.printf("0x%x ",bytes[i]);
      }
   }
}

关闭文件操作         

当我们把所有的文件操作都完成之后,那么我们就得关闭文件,使用inputStream.close()来防止文件资源泄露。但如果在执行这段代码之前就发生了异常,导致程序提前终止,那么这个文件就不会被关闭,所以我们需要使用异常处理机制来解决这个问题。

try {
	//...
} finally {
	inputstream.close();
}

通过try-finally语句,就可以保证finally中的代码无论如何都会被执行到。

但如果我们按照上面这种方法,可能就会写成:

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

public class Demo2 {
    /**
     * 程序的入口点。此方法用于读取指定文件并将其内容输出到控制台。
     * 
     * @param args 命令行参数,本程序未使用。
     */
    public static void main(String[] args) {
        // 尝试打开一个文件输入流以读取文件内容。
        try {
            InputStream inputStream=new FileInputStream("./text.txt");
            // 创建一个字节数组,用于存储从文件读取的数据。
            byte[] bytes=new byte[1024];
            // 读取文件内容到字节数组中。
            int len=inputStream.read(bytes);
            // 循环读取文件,直到文件末尾。
            while (len!=-1){
                // 将读取的字节数组转换为字符串并打印。
                System.out.println(new String(bytes,0,len));
                // 继续读取文件的下一部分内容。
                len=inputStream.read(bytes);
            }
        } 
        // 如果文件不存在,抛出运行时异常。
        catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } 
        // 如果发生IO错误,抛出运行时异常。
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

 这样看起来是不是不雅观?

那么有没有一种方法能够让我们的方法看起来更加简洁呢?

try (InputStream inputStream=new FileInputStream("./text.txt")) {
	//....
}

上面这种语句叫做 try-with-resources 语句,这个语句的特性:当括号里的对象实现了 closerable 的话,当{}内的代码执行完之后会自动调用该对象的 close() 方法。

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

public class Demo3 {
    /**
     * 程序的入口点
     * 该方法用于读取指定文件并打印出文件内容的十六进制表示
     * @param args 命令行参数,本程序未使用此参数
     */
    public static void main(String[] args) {
        // 尝试打开一个文件输入流以读取文件内容
        try(InputStream inputStream=new FileInputStream("./text.txt")){
            // 创建一个字节数组,用于存储从文件读取的数据
            byte[] bytes=new byte[1024];
            // 读取文件内容到字节数组中
            int len=inputStream.read(bytes);
            // 遍历已读取的字节数组,以十六进制格式打印每个字节
            for(int i=0;i<len;i++){
                System.out.printf("0x%x ",bytes[i]);
            }
        } catch (IOException e) {
            // 捕获可能发生的IO异常,并打印异常信息
            e.printStackTrace();
        }
    }
}

为什么不关闭文件,会造成文件资源泄露?

在前面,我们学习了进程和线程,当我们创建一个线程后,就有一个对应的PCB(进程控制块),而PCB中存在着多种属性,例如pid、、内存指针、文件描述表等。当在一个进程中打开一个文件,就需要在这个文件描述符表中分配一个元素,而这些元素存在一个长度固定的数组中,如果我们打开一个文件,并且不将文件关闭,这个数组中的元素就会越来越多,直到数组元素满了,就会出现文件资源泄露问题。 

所以我们在用完文件之后,一定要关闭文件!

利用Scanner进行字符获取

在上述的例子中,其实我们可以看到对字符类型的直接使用InputStream进行读取是非常麻烦且困难的,所以,我们可以使用一种我们之前比较熟悉的类来完成该工作,就是Scanner类

构造方法说明
Scanner(InputStream,String charset)使用charset字符集进行is的扫描读取

charset使用的是系统默认字符编码来读取。

我们可以使用下面的代码来查看系统的默认编码方式。

Charset defaultCharset = Charset.defaultCharset();
System.out.println("Default charset: " + defaultCharset.name());

 示例:

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

public class Demo4 {
    /**
     * 程序的入口点
     * 该方法用于读取指定文件中的内容,并将其打印到控制台
     * 
     * @param args 命令行参数,本例中未使用
     */
    public static void main(String[] args) {
        // 尝试打开文件 "./text.txt" 以读取其内容,在文本内写入:你好
        try(InputStream inputStream=new FileInputStream("./text.txt")){
            // 创建扫描器以读取输入流中的数据
            Scanner scanner=new Scanner(inputStream);
            // 循环读取并打印文件内容,直到文件末尾
            while(scanner.hasNext()){
                System.out.printf("%s",scanner.next());
            }
        }catch (IOException e){
            // 捕获并处理可能的IO异常
            e.printStackTrace();
        }
    }

}


OutputStream字节流输出

OutputStream也是一个抽象类,是所有字节输出流的父类。

方法

修饰符及返回值类型方法签名说明
voidwrite(int b)写入要给字节的数据
voidwrite(byte[] b)将b这个字符数组中的数据全部写入os中
intwrite(byte[] b,int off,int len)将b这个字符数组中从off开始的数据写入到os中,一共写len个
voidclose()关闭字节流
voidflush()重要:我们知道I/O的速度是很慢 的,所以,大多的OutputStream 为了减少设备操作的次数,在写数 据的时候都会将数据先暂时写⼊内 存的⼀个指定区域⾥,直到该区域 满了或者其他指定条件时才真正将 数据写⼊设备中,这个区域⼀般称 为缓冲区。但造成⼀个结果,就是 我们写的数据,很可能会遗留⼀部 分在缓冲区中。需要在最后或者合 适的位置,调⽤flush(刷新)操 作,将数据刷到设备中。

OutputStream同样只是⼀个抽象类,要使⽤还需要具体的实现类。我们现在还是只关⼼写⼊⽂件 中,所以使⽤FileOutputStream

文件输出流--FileOutputStream

既然我们知道了如何以字节为单位来读取文件数据,那么就来学习如何输出字节到文件中。

构造方法

签名说明
FileOutputStream(File file)利用文件构造文件输入流根据传入的文件,创建一个文件输出流对象
FileOutputStream(File file,boolean append)利用文件构造文件输入流创建一个文件输出流对象,若append为true,说明保留文件中的内容,不清除
FileOutputStream(String name)利用文件路径构造文件输入流
FileOutputStream(String name,boolean append)利用文件路径构造文件输入流创建一个文件输出流对象,若append为true,说明保留文件中的内容,不清除

首先需要构造一个OutputStream对象。同样的,这里我们使用FileOutputStream来实例化一个输出流对象。这里同样需要抛出IO异常。

  try(OutputStream outputStream=new FileOutputStream("./text.txt")){
            //.....
        }catch (IOException e){
            e.printStackTrace();
        }

理解OutputStream中的write方法

我们知道OutputStream其实是一个抽象类,所以其中的方法也是需要其子类来实现,这里我们将FileOutputStream类。

我们可以看到其实与InputStream中的read的方法相似,都是由三个重载的方法.

我们可以看到当参数为int的时候,其实b的值对于的是ASCII码中的字符,其余两个方法的参数,与InputStream类似。

示例:假设现在我们要在一个空文本text中写入“你好”,那么我们可以查看其对于的16进制值。

0xe4 0xbd 0xa0 0xe5 0xa5 0xbd 

write(int b):一次写入一个字节的数据

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

public class Demo5 {
    /**
     * 程序的入口点 主函数
     * 将一组Unicode字符写入到文件中
     * 
     * @param args 命令行参数,本程序中未使用
     */
    public static void main(String[] args) {
        // 使用try-with-resources语句确保文件流正确关闭
        try(OutputStream outputStream=new FileOutputStream("./text.txt")){
            outputStream.write(0xe4);
            outputStream.write(0xbd);
            outputStream.write(0xa0);
            outputStream.write(0xe5);
            outputStream.write(0xa5);
            outputStream.write(0xbd);
        }catch (IOException e){
            // 捕获并处理可能的IO异常
            e.printStackTrace();
        }
    }

}

运行前:

运行后:


当然,这样一个字节一个字节写入效率是很低的,所有我们可以使用字节数组。

write(byte[] b):最多读取b.length字节的数据到b中,返回实际读取到的数量;-1代表已经读完了

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

public class Demo6 {
    /**
     * 主函数,用于向指定文件写入字节数据
     * @param args 命令行参数,本例中未使用
     */
    public static void main(String[] args) {
        // 使用try-with-resources语句确保文件流正确关闭
        try(OutputStream outputStream=new FileOutputStream("./text.txt",true)){
            // 定义要写入文件的字节数组,此处为"你好"的UTF-8编码
            byte[] bytes={(byte) 0xe4, (byte) 0xbd, (byte) 0xa0, (byte) 0xe5, (byte) 0xa5, (byte) 0xbd};
            // 将字节数组写入文件
            outputStream.write(bytes);
        }catch (IOException e){
            // 捕获并处理可能的IO异常
            e.printStackTrace();
        }
    }

}

这里我们可以在构造的时候传入一个true,不删除文本原有内容。且这里需要注意的是:

由于数组是字节型,但是我们16进制表示的时候,其实是整型,所以我们这里存储的时候需要强转为字节类型。

运行前:

运行后:


write(byte[] b,int off,int len):将b这个字符数组中从off开始的数据写入到os中,一共写len个

接着上面的程序,我们想要拼接个“

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

public class Demo7 {
    /**
     * 程序的入口点
     * 使用try-with-resources语句确保文件流正确关闭,以向文件追加写入字节
     * @param args 命令行参数,本程序不使用此参数
     */
    public static void main(String[] args) {
        // 使用try-with-resources语句确保文件流正确关闭
        try(OutputStream outputStream=new FileOutputStream("./text.txt",true)){
            // 定义要写入文件的字节数组,此处为"你好"的UTF-8编码
            byte[] bytes={(byte) 0xe4, (byte) 0xbd, (byte) 0xa0, (byte) 0xe5, (byte) 0xa5, (byte) 0xbd};
            // 将字节数组写入文件
            outputStream.write(bytes,3,3);
        }catch (IOException e){
            // 捕获并处理可能的IO异常
            e.printStackTrace();
        }

    }

}

 运行前:

运行后:

有时候我们会看见我们运行程序之后,但是我们查看的时候,并没有写入的数据,这是为什么?

其实在写入数据的时候,并不是直接将内存中的数据写入文件中的,而是在这个过程中还会经过一个特殊的内存空间--缓冲区。当数据传入缓冲区的时候,缓冲区并不会立即将数据传输到文件中,而是当缓冲区中的数据达到一定大小之后,才会将这些数据一起传输到文件中。

为什么要有缓冲区这个东西呢?

数据从内存到缓冲区中的速度是很欢的,但是数据从缓存区到文件中速度是比较慢的。如果缓冲区中接收到了数据之后就立即将这些数据传输到文件中,这样的I/O次数就会很多,效率自然也高不了。就类似于:你的领导给你分配了一个任务,那么你就去执行,但在这个过程中,由于领导又临时给你分配了一些任务,如果你在这个过程中,完成一个任务就上交给领导一次,这样不仅效率低,领导对你的好感也会降低,所以,可以选择把任务都完成之后再交给领导,这样效率就提高了很多。这就是缓冲区的作用。

我们既然知道了数据在写入的过程中,会经过缓冲区,那么为什么在写入数据的时候,是数据最终没有被写入?这就是由于缓冲区内的数据太少,没有达到数据传入文件的大小,那么缓冲区就不会将接收到的数据传入文件,而这时由于程序执行结束,那么缓冲区内的数据就会被释放掉,导致数据最终没有被写入文件中。

因此,为了解决这个问题,我们在把数据都写入后,可以刷新一下缓冲区。outputstream.flush().

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

public class Demo7 {
    /**
     * 程序的入口点
     * 使用try-with-resources语句确保文件流正确关闭,以向文件追加写入字节
     * @param args 命令行参数,本程序不使用此参数
     */
    public static void main(String[] args) {
        // 使用try-with-resources语句确保文件流正确关闭
        try(OutputStream outputStream=new FileOutputStream("./text.txt",true)){
            // 定义要写入文件的字节数组,此处为"你好"的UTF-8编码
            byte[] bytes={(byte) 0xe4, (byte) 0xbd, (byte) 0xa0, (byte) 0xe5, (byte) 0xa5, (byte) 0xbd};
            // 将字节数组写入文件
            outputStream.write(bytes,3,3);
            outputStream.flush();
        }catch (IOException e){
            // 捕获并处理可能的IO异常
            e.printStackTrace();
        }

    }

}

利⽤PrintWriter找到我们熟悉的⽅法

上述,我们其实已经完成输出⼯作,但总是有所不⽅便,我们接来下将OutputStream处理下,使⽤ PrintWriter 类来完成输出,因为 PrintWriter 类中提供了我们熟悉的print/println/printf方法。

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

public class Demo8 {
    /**
     * 程序的入口点 主要用于演示如何向文件写入文本内容
     * @param args 命令行参数,本例中未使用
     */
    public static void main(String[] args) {
        // 使用try-with-resources语句确保文件输出流在使用后能被正确关闭
        try(OutputStream outputStream=new FileOutputStream("./text.txt")){
            // 创建打印流,用于向文件输出流中写入可读的文本格式
            PrintStream printStream=new PrintStream(outputStream);
            // 向文件中写入并换行,以展示良好的文本格式
            printStream.println("你好");
            // 继续向文件中写入文本,不换行,以连续书写句子
            printStream.print("世界");
            // 使用格式化输出向文件中写入欢迎信息,增强文本的可读性
            printStream.printf("\n欢迎来到程序人生");
        }catch (IOException e){
            // 捕获并处理可能发生的IO异常,避免程序因未捕获异常而终止
            e.printStackTrace();
        }
    }

}

就此,我们关于如何以字节为单位对文件进行读取和写入操作就讲完了。

那么接下来,我们就来讲解如何以字符为单位对文件进行读取和写入操作,其实也就是对字节流的包装。


Reader读操作

Reader是一个抽象类,是所有字符输出流的父类

所以如果我们要使用,需要使用其子类。我们可以看到,直接继承Reader的类有:

这里我们讲的是文件操作,所以我们使用FileReader类 。

我们可以看到,其实看起来像是FileReader继承了inputStreamReader,但其实不是。根据 Java 文档,FileReader 是直接继承自 InputStreamReader 的父类 Reader,并且它内部使用了一个 InputStreamReader 实例来完成从文件到字符流的转换。

FileReader 类的目的是为了简化从文件中读取字符的过程。它使用平台默认的字符编码来创建一个 InputStreamReader,这样用户就不需要关心底层的编码细节。FileReader 提供了一个更简便的方式来读取文件中的文本数据。

创建Reader对象

那么我们就可以根据Fileder类来实例化一个Reader对象。

FileReader的构造方法

签名说明
FileReader(File  file)根据文件来构造FileReader对象。
FileReader(String name)根据文件路径来构造FileReader对象

这里我们选择第二种方法:

try(Reader reader=new FileReader("./text.txt")){
            //...
        }catch (IOException e){
            e.printStackTrace();
        }

 创建 Reader 的过程就是打开文件的过程,如果文件不存在就会创建失败。

理解Reader类中的不同read方法

当我们创建出Reader对象之后,那么我们就可以读取文件中的内容了。 

修饰符及返回值类型方法签名说明
intread()一次读取一个字符的数据,返回-1说明已经读完了
intread(char[] cbuf)一次读取若干个字符,直到把这个cbuf数组给填满
intread(char[] cbuf,int off,int len)一次读取若干个字符,并且将读取到的数据从 cbuf 的off位置开始,len长度的大小填充满
voidclose()关闭字符流

这里我们所需要的是字符,那么他为什么不用char作为返回值类型呢?

在java中,char类型是用来表示字符的,是一个16位的Unicode字符,可以有65536个不同的字节数。但是,我们read()需要能够返回一个特殊的值来表示文件结尾(EOF ,End and File)。所以,我们如果使用char类型,那么我们就无法表示这个特殊值。

为了解决这个问题,read()使用int作为返回类型。在java中,int是一个32位的整型,可以用来表示更大的范围。在Reader类中,read()返回废是一个int类型的值,其中0~65536表示实际的字符,而01表示文件结尾(EOF).

所以,在java中,Reader类的read()方法返回int类型是因为该方法需要能够表示所有可能的字符以及特殊值

既然知道了read()的返回值为什么是int后,那么为什么返回的int值是2个字节的长度而不是三个字节的长度?

在java标准库 ,对字符编码做了很多操作。

如果我们只使用char,此时的字符集就是Unicode,每个字符都是2个字节;

如果我们使用String,此时就会把字符的Unicode编码转为UTF-8,在utf-8中,一个英文字符是1个字节,一个中文字符是3个字节。

char[] c = {'a','b','c'};
String s = new String(c);

在上面代码中,c字符数组中一个字符占两个字节,但是如果将其转换为字符串的是,字符的编码方式就从Unicode转换为UTF-8编码了。

char cc=s.charAt(0);

当我们取其中一个字符,获取到的字符就会从UTF-8编码转化为Unicode编码。

使用Reader中的read()方法

1.read():一次读取一个字符,当到文件结尾会返回-1。

这个方法的返回值就是将读取到的字符转换为int类型。

Reader抽象类实现了Closeable接口,所以可以使用try-with-resources语句。 

示例:假设现在有文本文件text,在文本内存放:“你好”。

 

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

public class Demo9 {
    /**
     * 程序的入口点
     * 该方法打开并读取本地文件系统中的文本文件,并将文件内容打印到控制台
     * 
     * @param args 命令行参数,本例中未使用
     */
    public static void main(String[] args) {
        // 尝试打开文件并创建一个Reader对象用于读取文件内容
        try(Reader reader=new FileReader("./text.txt")){
            // 循环读取文件中的每个字符,直到文件末尾
            while(true) {
                // 读取下一个字符,如果达到文件末尾,则返回-1
                int n = reader.read();
                // 如果达到文件末尾,则停止读取
                if(n==-1) break;
                // 打印读取的字符到控制台
                System.out.printf("%c",(char)n);
            }
        }catch (IOException e){
            // 捕获并处理可能的IO异常,例如文件不存在或无法读取
            e.printStackTrace();
        }
    }

}


2.read(char[] cbuf):一次读取多个字符,将字符读取到cbuf字符数组中。

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

public class Demo10 {
    /**
     * 程序的入口点
     * 该方法用于读取指定文本文件的内容,并将其输出到控制台
     * 
     * @param args 命令行参数,本程序未使用
     */
    public static void main(String[] args) {
        // 尝试读取名为text.txt的文件内容
        try(Reader reader=new FileReader("./text.txt")) {
            // 创建一个能够一次读取1024个字符的字符数组
            char[] chars = new char[1024];
            // 读取文件内容到字符数组中
            int len = reader.read(chars);
            // 遍历已读取的字符数组,输出每个字符到控制台
            for(int i = 0; i < len; i++) {
                System.out.printf("%c", chars[i]);
            }
        } catch (IOException e) {
            // 如果在文件读取过程中发生错误,打印异常信息
            e.printStackTrace();
        }
    }

}

 

第三个方法与我们前面讲的InputStream中的read方法是一样的意思的,不过Reader中读取的是一个字符,而InputStream中read读取的是一个字节。

利用Scanner进行字符获取

对于上面这样读取字符,可能效率比较低,所以我们也可以使用Scanner来进行字符获取。

示例:现有一个文本内让内容如下,要求使用Scanner进行读取。

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

public class Demo13 {
    /**
     * 程序的入口点
     * 该方法用于读取指定文本文件的内容,并将其输出到控制台
     *
     * @param args 命令行参数,本程序未使用
     */
    public static void main(String[] args) {
        // 尝试读取名为text.txt的文件内容
        try(Reader reader=new FileReader("./text.txt")) {
            Scanner scanner=new Scanner(reader);
            // 循环读取文件的每一行内容,并输出到控制台
            while(scanner.hasNext()){
                System.out.println(scanner.next());
            }
        } catch (IOException e) {
            // 如果在文件读取过程中发生错误,打印异常信息
            e.printStackTrace();
        }
    }

}

 


Writer写操作

Writer是一个抽象类,是所有字符输出流的父类。

 与Reader所讲的差不多,这里进行文件操作,我们需要使用FileWriter

创建Writer对象

FileWriter构造方法

签名说明
FileWriter(String name)根据文件路径来构造FileWriter对象
FileWriter(String name,boolean append)根据文件路径来构造FileWriter对象,若append为true,说明保留文件中的内容,不清除
FileWriter(File file)根据文件来构造FileWriter对象
FileWriter(File file,boolean append)根据文件来构造FileWriter对象,若append为true,说明保留文件中的内容,不清除

这里我们使用通过指定文件路径来创建Writer对象。

理解Writer中的write方法

此处的write方法其实与我们前面讲的OutputStream的write用法基本相同。不过此处比OutputStream的write多了两个额外的重载的方法,可以直接写入字符串,这样就方便多了。

当我们指定的文件不存在,那么Writer就会帮我们创建一个文件。

这里由于和OutputStream的write类似,所以我们只讲2个字节流中没有的。

使用Writer中的write方法

1.write(String str):给文件写入一个字符串。

示例:从控制台输入一段字符串并写入到text.txt文本文件中,(若文本不存在,会自动创建


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

public class Demo11 {
    /**
     * 程序的入口点
     * 从标准输入读取一行文本,并将其写入到文件中
     *
     * @param args 命令行参数,本程序中未使用
     */
    public static void main(String[] args) {
        // 创建Scanner对象,用于读取标准输入
        Scanner scanner=new Scanner(System.in);
        // 使用try-with-resources语句创建FileWriter对象,用于写入文件,自动管理资源
        try(Writer writer=new FileWriter("./text.txt")){
            // 读取标准输入中的一行文本
            String str=scanner.nextLine();
            // 将读取的文本写入文件
            writer.write(str);
        }catch (IOException e){
            // 捕获IOException,打印异常信息
            e.printStackTrace();
        }
    }

}

 运行中(等待输入):

 运行结束:

 


2.1.write(String str,int off ,int len):给文件写入一个从off位置开始,长度为len的字符串。

示例:这里我们接着上面的例子,拼接上一段字符串。

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

public class Demo12 {
    public static void main(String[] args) {
        // 创建Scanner对象,用于读取标准输入
        Scanner scanner=new Scanner(System.in);
        // 使用try-with-resources语句创建FileWriter对象,用于写入文件,自动管理资源
        try(Writer writer=new FileWriter("./text.txt",true)){
            // 读取标准输入中的一行文本
            String str=scanner.nextLine();
            // 将读取的文本写入文件
            writer.write(str,2,2);
        }catch (IOException e){
            // 捕获IOException,打印异常信息
            e.printStackTrace();
        }
    }
}

运行中(待输入):

 

运行后:


既然我们已经学习了文件的基本操作+文件内容的读写操作,那么就下来,我们就可以来实现一些小工具来锻炼我们的能力。

运用文件操作和IO实现一个删除指定文件的功能

要求:实现一个扫描指定目录,并且找到名称中包含指定字符的所有普通文件(不包括目录),并且后序访问用户是否要删除该文件。

思路:根据要求,我们要在一个指定的目录下寻找包含指定字符的文件,并询问是否要删除。我们知道文件底层其实是使用树形结构,那么我们就需要使用递归,当遇到目录的时候,就继续递归;当递归遇到文件的时候,就判断这个文件是否包含用户指定的字符。如果包含那么进行询问是否要删除。

代码实现:

import java.io.File;
import java.util.Scanner;

/**
 * Demo类用于演示如何在指定目录中搜索并删除包含特定关键词的文件
 */
public class Demo {
    /**
     * 程序的入口点
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 创建Scanner对象,用于读取用户输入
        Scanner sc = new Scanner(System.in);
        // 提示用户输入目录路径
        System.out.println("请输入你要扫描的目录路径:");
        // 读取用户输入的目录路径
        String path=sc.next();
        // 根据用户输入的路径创建File对象
        File root=new File(path);
        // 检查所给路径是否为目录
        if(!root.isDirectory()){
            System.out.println("输入的路径不是目录");
            return;
        }
        // 提示用户输入要删除的文件的关键词
        System.out.println("请输入你要删除的文件的关键词:");
        // 读取用户输入的关键词
        String dest=sc.next();

        // 调用fileSearch方法,在指定目录中搜索包含关键词的文件
        fileSearch(root,dest);
    }

    /**
     * 递归搜索指定目录下的文件,并对匹配关键词的文件进行删除确认
     * @param root 搜索的根目录
     * @param dest 要匹配并删除的文件的关键词
     */
    private static void fileSearch(File root, String dest) {
        // 列出目录下的所有文件和子目录
        File[] files=root.listFiles();
        // 如果该目录为空,则直接返回
        if(files==null) return;
        // 遍历目录中的每个文件或子目录
        for(File file:files){
            // 如果是文件,则检查是否匹配关键词并进行删除确认
            if(file.isFile()){
                doDelete(file,dest);
            }else{
                // 如果是目录,则递归搜索
                fileSearch(file,dest);
            }
        }
    }

    /**
     * 检查文件名是否包含关键词,如果包含则提示用户是否删除
     * @param file 待检查的文件
     * @param dest 关键词
     */
    private static void doDelete(File file, String dest) {
        // 检查文件名是否包含关键词
        if(file.getName().contains(dest)){
            // 提示用户找到匹配文件,并询问是否删除
            System.out.println("当前文件为:"+file.getAbsoluteFile()+", 是否要删除(Y/N)?");
            // 创建Scanner对象,用于读取用户输入
            Scanner sc=new Scanner(System.in);
            // 读取用户的删除选择
            String choice=sc.next();
            if(choice.equals("Y")){
                // 如果用户选择删除,则执行删除操作
                file.delete();
                System.out.println("删除成功");
            }else{
                System.out.println("取消删除");
            }
        }
    }
}

 

复制指定文件

进⾏普通⽂件的复制,通过传入一个要被复制的文件的路径,判断是否是文件,如果是文件,则进行下一步操作;反之,则直接返回。输入要复制到的文件路径,判断该文件是否存在,如果存在就需要判断是否要进行覆盖;反之则直接返回。通过字节流来复制。

import java.io.*;
import java.util.Scanner;

/**
 * Demo1类用于演示文件复制的功能
 */
public class Demo1 {
    /**
     * 主函数,用于处理文件复制的流程
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 创建Scanner对象,用于从命令行读取用户输入
        Scanner sc = new Scanner(System.in);
        
        // 提示用户输入源文件路径,并读取
        System.out.println("请输入你要复制的文件的路径:");
        String src = sc.next();
        // 根据输入的路径创建File对象
        File scFile = new File(src);
        
        // 检查源文件是否存在
        if (!scFile.exists()) {
            System.out.println("您输入的文件不存在,请确认文件路径的正确性");
            return;
        }
        
        // 检查源文件是否为文件夹
        if (scFile.isDirectory()) {
            System.out.println("您输入的是一个文件夹,请输入一个文件路径");
            return;
        }
        
        // 提示用户输入目标文件路径,并读取
        System.out.println("请输入你要复制到的目标路径:");
        String dest = sc.next();
        // 根据输入的目标路径创建File对象
        File destFile = new File(dest);
        
        // 检查目标路径是否已存在文件
        if (destFile.exists()) {
            // 如果目标路径是一个目录而不是文件,则提示错误
            if (destFile.isDirectory()) {
                System.out.println("您输入的路径是一个目录,不是文件,请确认文件路径是否正确");
                return;
            }
        }
        
        // 如果源文件是文件,则进行以下操作
        if (scFile.isFile()) {
            // 如果目标文件已存在,则询问用户是否覆盖
            if (destFile.exists()) {
                System.out.println("您要复制到的文件已经存在,是否需要覆盖:Y/N");
                String choice = sc.next();
                // 如果用户选择不覆盖,则退出程序
                if (!choice.equalsIgnoreCase("Y")) return;
            }
        }

        // 使用try-with-resources语句复制文件内容
        try (InputStream inputStream = new FileInputStream(scFile);
             OutputStream outputStream = new FileOutputStream(destFile)) {
            // 创建字节数组用于读取文件内容
            byte[] bytes = new byte[1024];
            // 循环读取源文件内容直到末尾
            while (true) {
                // 读取字节数组的下一个块
                int n = inputStream.read(bytes);
                // 如果读取到文件末尾则退出循环
                if (n == -1) break;
                // 将读取的内容写入目标文件
                outputStream.write(bytes, 0, n);
            }
        } catch (IOException e) {
            // 处理异常
            e.printStackTrace();
        }
    }
}

示例:复制图片

现有一张图片,我们想要给他复制一张那么我们就得找到其相应的文件路径

输入对应的文件路径,并指定一个目标路径

 

在指定目录中查找文件名或文件内容含有关键词的文件

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Demo2 {
    /**
     * 主函数,用于交互式输入目录和关键词,并打印出包含关键词的文件
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入你要扫描的目录:");
        String root = sc.next();
        File rootFile = new File(root);
        if (!rootFile.exists()) {
            System.out.println("目录不存在");
            return;
        }
        if (!rootFile.isDirectory()) {
            System.out.println("您输入的路径不是正确的目录路径,请检查你输入的路径");
            return;
        }
        System.out.println("请输入你要查找的文件名或者文件内容中包含中包含的关键词:");
        String word = sc.next();

        List<File> list = new ArrayList<>();
        Serach(rootFile, word, list);
        for (File file : list) {
            System.out.println(file.getAbsoluteFile());
        }
    }

    /**
     * 递归搜索目录下包含特定关键词的文件
     * @param rootFile 根目录文件对象
     * @param word 关键词
     * @param list 保存匹配文件的列表
     */
    private static void Serach(File rootFile, String word, List<File> list) {
        File[] files = rootFile.listFiles();
        if (files == null) return;
        for (File file : files) {
            if (file.isDirectory()) {
                Serach(file, word, list);
            } else {
                if (isContainsContent(file, word)) {
                    list.add(file);
                }
            }
        }
    }

    /**
     * 检查文件是否包含特定内容
     * @param file 待检查的文件
     * @param word 关键词
     * @return 如果文件名或文件内容包含关键词,返回true;否则返回false
     */
    private static boolean isContainsContent(File file, String word) {
        if (file.getName().contains(word)) return true;
        StringBuilder sb = new StringBuilder();
        try (InputStream inputStream = new FileInputStream(file)) {
            Scanner scanner = new Scanner(file);
            while (scanner.hasNextLine()) {
                sb.append(scanner.nextLine());
                if (sb.indexOf(word) != -1) return true;
            }
            return false;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}


以上就是本篇IO操作的所有内容啦,若有不足欢迎指正~

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小猪同学hy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值