本篇主要介绍Java中文件IO的相关内容
目录
一、什么是文件
文件是存储在计算机存储设备上的数据单元,一个文本,一张图片,一个音频....这些都可以被称为是文件。文件的数据格式非常丰富,.jpg、.txt 、.img 、.mp3.....,这些数据格式通常可以规为两种,一种是文本文件,一种是二进制文件。文本文件储存的是一个一个字符,而二进制文件储存的则是一个一个二进制数字。我们如何区分这两种文件呢?
通过记事本打开一个文件,如果里面是下面这种我们能够读懂的文字,那这就是一个文本文件。
如果打开的是一堆我们看不懂的文字和符号,那这个文件就是二进制文件。
文件的路径有两种,一种是以盘符为起始位置的绝对路径,另一种则是以当前目录为起始位置的相对路径。
我们通过文件管理器打开一个文件,在最上方的导航框内显示的就是绝对路径,\ 代表下一级路径(Windows \ / 都代表下一级路径),: 代表在哪个盘里
而以 . 或者 / 为起始的路径就是相对路径,其中 . 代表当前路径, .. 代表上一级路径,同理再加一个点就代表更上一级的路径,以 / 开头是对单个 . 的省略写法。
二、操作文件
在Java中,通过File类来表示一个文件或者文件夹,File类位于Java.io包下,File类的构造方法如下
构造方法 | 作用 |
File(String pathname) | 根据文件路径打开文件 |
File(String parent,String child) | 根据父路径和子路径来打开文件 |
File(File parent.String child) | 根据父文件和子路径打开文件 |
传入的文件路径可以是绝对路径,也可以是相对路径,如果是相对路径,在Idea中以当前项目所在的目录为工作目录,在其他环境则是以当前执行代码的命令行所在目录作为工作目录。如果路径所表示的文件或者文件夹不存在,则可以通过File的相关方法进行创建。
在File类中,封装了许多操作文件和获取文件信息的方法,例如:
获取文件信息的方法有:
方法名 | 作用 |
getParent() | 获取文件的父目录 |
getName() | 获取文件名称 |
getPath() | 获取文件路径 |
getAbsolutePath() | 获取文件绝对路径 |
通过代码测试以下这些方法
运行结果:
对文件属性进行判断的方法(返回值均为boolean类型)有:
方法 | 作用 |
exist() | 判断文件是否存在 |
isDirectory() | 判断文件是否为文件夹 |
isFile() | 判断是否为文件 |
isHidden() | 判断文件是否为隐藏文件 |
isAbsolute | 判断在创建File时传入的路径是否为绝对路径 |
canRead() | 判断用户是否对文件有可读权限 |
canWrite() | 判断用户是否对文件有可写权限 |
canExecute() | 判断用户是否对文件有执行权限 |
代码测试:
执行结果
对文件的创建和删除的方法有
方法 | 作用 |
boolean createNewFile() | 创建File所代表的文件 |
boolean mkdir() | 创建File所代表的目录(可以是多级目录) |
boolean delete() | 删除File所代表的文件 |
void deleteOnExist() | 在代码运行结束后删除File所代表的文件 |
这里我们来演示一下删除前面测试代码里的test.doc 文件
执行代码前
执行后
然后我们再通过createNewFile再重新创建一个retest.doc(使用这个方法需要处理IO异常)
执行前
执行后
接下来我们再试试创建一个tests文件夹
执行前:
执行后:
其他方法:
方法 | 作用 |
String[] list() | 获取目录下所有文件的文件名 |
File[] listFiles() | 以File对象的形式获取目录下的所有文件 |
booelan renameTo() | 修改文件名 |
代码示例
运行结果:
打开文件管理器可以发现tests1已经成功改名了
三、读写文件
流对象
Java中读写文件是通过流对象来完成,什么是流对象呢?
在解释什么是流对象之前,我们先来了解一下什么是流。流,和水流类似,一点点的流动,并且延绵不断,具体可以理解为数据以字节/字符的方连续不断的从一个地方传入到另一个地方,流传输的数据数量并不确定,你可以一次流动很多字节/字符,也可以只流动一个字节/字符,就像水一样,你可以一次取很多,也可以一次只取一点。
而流对象就是对流进行封装后的对象。流通常分为两种,一种是输出(写)流,一种是输入(读)流,输入输出是以CPU为参照的,数据如果是从其他设备往靠近CPU的方向流动,那这就是输入流,如果是往远离CPU的方向流动,那就是输出流,例如从硬盘加载数据到内存,就是输入流,反过来就是输出流。因为通常认为,内存相较于硬盘更接近CPU.
因此流对象又可以根据流的不同分为输入流对象和输出流对象,并且流对象根据传输的数据的类型又划分出了以字符形式传输 的字符流和以二进制形式传输的字节流。(注意:进行流对象的相关操作时通常需要处理IO异常)
字符流
字符输入流
Java中的字符输入流为Reader,Reader是一个接口,他的实现类为FileReader,FileReader的构造方法为:
构造方法 | 作用 |
FileReader(File file) | 根据源文件对象创建字符输入流 |
FileReader(File file,Charset charset) | 根据源文件对象创建字符输入流,并指定字符集 |
FileReader(String fileName) | 根据源文件路径创建字符输入流 |
FileReader(String fileName,CharSet charset) | 根据源文件路径创建字符输入流,并指定字符集 |
Reader的主要方法如下:
方法 | 作用 |
int read() | 读取一个字符数据,返回值为读到的字符值,读到-1代表全部读完 |
int read(char[] b) | 一次最多读取b.len个字符,返回值为读到的字符数,读到-1代表读完 |
int read(char[] b,int off,int len) | 一次最多读取len - off个字符,返回值为读到的字符数,并从b的off位置开始放,读到-1代表读完 |
void close() | 关闭流 |
字符输出流
Java中的字符输出流为Writer,它的实现类为FileWriter,构造方法为:
构造方法 | 作用 |
FileWriter(File file) | 根据文件对象创建字符输出流 |
FileWriter(File file ,boolean append) | 根据文件对象创建字符输出流,并设置是否追加写 |
FileWriter(String filePath) | 根据文件路径创建字符输出流 |
FileWriter(String filePath,boolean append) | 根据文件路径创建字符输出流,并设置是否追加写 |
Writer的方法如下:
方法 | 作用 |
void write(int b) | 写入一个字符数据 |
void write(char[] b) | 将数组b的数据全部写入 |
int write(char[] b , int off ,int len) | 将数组b从off到len的数据写入 |
void close() | 关闭字符流 |
void flush() | 冲刷缓冲区 |
(众所周知,一次IO操作是非常耗时的,大多数输出流为了提升效率,并不会直接将数据写到文件里,而是先将数据保存到一个特定的区域,直到这个区放满才会将数据一次性写到文件中,这个数据就是缓冲区,而flush就是将缓冲区的数据直接冲刷到文件中,执行close方法时也是会进行一次冲刷缓冲区的操作)
接下来我们来实际使用一下这两个字符流
准备两个文本文件,text1,text2,往text1中写入数据"hello",然后通过ReaderFile读取text1的数据,并将读取到的数据写入到text2.
代码如下
(这里的buffer如果放满了,代表一次读取完成,当进行下一次读取,下一次读取的数据会覆盖buufer前面读取到的数据)
执行完后打开text2,也能到"hello"了
字节流
字节输入流
Java中的字节输入流为InputStrem,它的实现类为FileInputStrem,构造方法如下:
构造方法 | 作用 |
FileInputStrem(File file) | 根据文件对象创建字节输入流 |
FileInputStrem(String filePath) | 根据文件路径创建字节输入流 |
InputStrem的主要方法如下:
方法 | 作用 |
int read() | 读取一个字节的数据,返回值就是读取到的字节值,返回1代表读完 |
int read(byte[] b) | 一次最多读取b.len个字节,返回值为读到的字节数,读到-1代表读完 |
int read(byte[],int off,int len) | 一次最多读取len - off个字节,返回值为读到的字节数,并从b的off位置开始读,读到-1代表读完 |
void close() | 关闭流 |
(读取一个字节数据的无参方法的返回值是int ,明明返回的是byte类型的数据,为什么要用int来接收了,因为读到-1代表读完,而byte能表示的数据范围为-128到127,如果返回的是-1的话,将不能确定是读取完了还是真的读到了一个数据-1,而使用int就能解决这个问题,使用int接收byte的数据,会使原来能读到数据范围变成0 - 255,因此也就避免了上述问题,所以这里用int作为返回值,前面的字符流的read()用int作为返回值也是这个原因)
Java的字节输出流为OutputStrem,实现类为FileOutputStrem,构造方法为
构造方法 | 作用 |
FileOutputStrem(File file) | 根据文件对象创建字节输出流 |
FileOutputStrem(File file ,boolean append) | 根据文件对象创建字节输出流,并设置是否追加写 |
FileOutputStrem(String filePath) | 根据文件路径创建字节输出流 |
FileOutputStrem(String filePath,boolean append) | 根据文件路径创建字节输出流,并设置是否追加写 |
OutputStrem的主要方法如下
方法 | 作用 |
void write(int b) | 写入一个字节数据 |
void write(byte[] b) | 将数组b的数据全部写入 |
int write(byte[] b , int off ,int len) | 将数组b从off到len的数据写入 |
void close() | 关闭流 |
void flush() | 冲刷缓冲区 |
接下来我们实操一下字节流的使用
将一张二进制的图片文件复制到另一个路径下
代码如下:
打开文件可以发现已经有imgs.webp了
(注意:我的电脑里原本是没有imgs.webp的,而为什么会写入成功呢,是因为输出流在写入文件时如果发现文件不存在,会自动依据构造方法传入的文件对象,或者文件路径创建一个目标文件,然后再进行数据写入)
Scanner搭配流对象
在Java中是可以通过Scanner搭配流对象进行数据读取的,在我们平常使用Scanner时,需要传入一个System.in,这其实就是一个流对象,因此我们可以传入一个我们自定义的流对象来实现对文件的读取。
接下来我们来常试一下通过Scanner来读取我们前面的text1文件
代码如下
打印结果:
打印流
在Java中存在打印流,打印流对输出流进行了一个封装,能够实现更加方便的写文件操作。
打印流有两种,一种为PrintStrem(字节打印流),另一种为PrintWrite(字符打印流),他们的构造方法如下:
构造方法 | 作用 |
PrintStrem(File file)/PrintWriter(File file) | 通过文件对象构建打印流 |
PrintStrem(OutputStrem out)/PrintWriter(Writer writer) | 通过输出流对象构建打印流 |
PrintStrem(OutputStrem out,boolean autoFlush)/PrintWriter(Writer writer,boolean autoFlush) | 通过输出流对象构建打印流,并设置是否自动刷新缓冲区 |
PrintStrem(String filePath, String charset) | 根据文件路径创建打印流,并设置字符编码 |
通常情况下,更加推荐使用字符打印流,因为字符打印流封装了字符流,以字符为单位进行数据传输,相当于字节打印流更加灵活。
下面我们来使用打印流往前面的text2里写几个数据,代码如下
执行完后可以发现,数据已经追加到text2了
注意事项
在前面介绍进程的时候,我们了解过,每一个进程都会对应一个PCB,在PCB中有一个重要的部分,文件资源符标,它里面记录了进程所拥有的文件资源,我们在进程中每打开一个文件,都会往文件描述符表中占用一个位置,但文件描述符表的位置数量是有限的,这也就意味着当文件描述符表被占满后,是无法继续正常获取文件资源的,因此,我们在使用完一个文件资源后,应当及时手动进行释放,但我们不是有GC帮我们自动进行对象回收吗,为什么还要手动释放呢?因为GC回收对象并不是即时的,它存在一定延迟,我们并不能保证GC每次都能及时帮我们回收,所以还是建议自己手动进行释放。
在这里给大家提供一个小技巧,我们可以将创建流对象的代码写在try-catch中的try后面的括号里,在try-catch执行完毕后会自动将前面写在括号里的流对象进行释放,具体代码格式如下:
(但要注意的是,不是每个对象都能被try自动释放,要被自动释放必须实现Closeable接口)
四、IO实战
下面我们通过上面所介绍的文件与流对象的知识,来进行实战一下:
实战内容为实现多线程文件拷贝
源码如下:
import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class FileIOTest {
//创建一个线程池来多线程的拷贝
ExecutorService pools = Executors.newFixedThreadPool(8);
//根据是否所有线程都已完成来判断是否拷贝完成
CountDownLatch latch = null;
//记录正在执行拷贝的线程数
AtomicInteger count = new AtomicInteger(0);
//实现将一个文件的内容拷贝到另一个文件
public void copyFile (File surFile, File destFile){
try (InputStream inputStream = new FileInputStream(surFile); OutputStream outputStream = new FileOutputStream(destFile)){
byte[] bytes = new byte[1024];
while (true){
int k = inputStream.read(bytes);
if (k == -1) break;
outputStream.write(bytes);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//将一个文件或文件夹或文件拷贝到另一个文件夹或文件夹
public void copyS(String sur , String dest){
latch = new CountDownLatch(1);
File surFile = new File(sur);
if (!surFile.exists()){
System.out.println("拷贝的源文件不存在!");
return;
}
File destFile = new File(dest);
if (!destFile.exists()) destFile.mkdir();
copy(surFile,destFile);
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("拷贝完成");
}
//拷贝的具体逻辑
public void copy(File surFile , File destFile){
if (surFile.isFile() && destFile.isFile()){
pools.submit(new Runnable() {
@Override
public void run() {
count.getAndIncrement();
copyFile(surFile,destFile);
count.getAndDecrement();
if (count.get() == 0){
latch.countDown();
}
}
});
}
if (surFile.isDirectory() && destFile.isDirectory()){
//罗列源文件夹的所有文件,如果是文件直接在目的文件夹中的同名文件(没有则创建)中进行拷贝,如果是文件夹,则递归的执行copy(),参数
// 为此文件夹和目的文件夹中的同名文件夹(没有则创建)
File[] list = surFile.listFiles();
for (File cur:
list) {
if (cur.isFile()){
//将拷贝文件的认务交给线程池来执行
pools.submit(new Runnable() {
@Override
public void run() {
count.getAndIncrement();
copyFile(cur,new File(destFile.getAbsoluteFile()+"/"+cur.getName()));
count.getAndDecrement();
//如果当前已经没有线程执行,则打表拷贝完毕,可以冲线
if (count.get() == 0){
latch.countDown();
}
}
});
}
else {
File destCur = new File(destFile.getAbsoluteFile()+"/"+cur.getName());
if(!destCur.exists()) destCur.mkdir();
//将对文件夹的拷贝交给线程池来执行
pools.submit(new Runnable() {
@Override
public void run() {
count.getAndIncrement();
copy(cur,destCur);
count.getAndDecrement();
if (count.get() == 0){
latch.countDown();
}
}
});
}
}
}
}
public static void main (String[] args) throws IOException{
FileIOTest fileIOTest = new FileIOTest();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入拷贝的源文件");
String sur = scanner.nextLine();
System.out.println("请输入拷贝的目的文件");
String dest = scanner.nextLine();
fileIOTest.copyS(sur,dest);
}
接下来我们随便拷贝一个文件测试一下,
控制台的输入如下:
然后我们再去拷贝的目的路径
可以发现拷贝成功了