Java中的文件操作可以分为两类,一类是对于 文件系统 的操作,例如获取文件路径信息、创建文件、删除文件等等;另一类是对于 文件内容 的操作,比如对于文件的读写操作。
1. 对文件系统的操作
1.1 操作方法
Java主要通过java.io.File
类对文件系统进行操作。
java.io包主要用于I/O操作,I就是Input代表输入,O就是Output代表输出,这里需要区分输入和输出的流向是以CPU的视角进行看待的,从硬盘流向CPU就是输入,从CPU流出就是输出。
我们先来看看File类的常见属性、构造方法、方法。
属性:
修饰符及类型 | 属性 | 说明 |
---|---|---|
static String | pathSeparator | 依赖于系统的路径分隔符,String类型标识 |
static char | pathSeparator | 依赖于系统的路径分隔符,char类型标识 |
构造方法:
签名 | 说明 |
---|---|
File(String pathname) | 根据文件路径创建一个File实例,路径可以是绝对路径,也可以是相对路径 |
File(String parent, String child) | 根据父级路径+孩子文件路径,创建一个File实例 |
File(File parent, String child) | 根据父目录+孩子文件路径,创建一个File实例 |
方法:
修饰符和返回类型 | 方法签名 | 说明 |
---|---|---|
String | getParent() | 获取File对象的父目录文件路径 |
String | getName() | 获取File对象的文件名称 |
String | getPath() | 获取File对象的文件路径 |
String | getAbsolutePath() | 获取File对象的绝对文件路径 |
String | getCanonicalPath() | 获取File对象的修饰文件路径 |
boolean | exists() | 判断File对象对应文件是否真实存在 |
boolean | isFile() | 判断File对象代表文件是否是普通文件 |
boolean | isDirectory() | 判断File对象代表文件是否是目录 |
boolean | createNewFile() | 根据File对象,自动创建一个空文件,成功返回true |
boolean | delete() | 根据File对象,删除该文件,删除成功返回true |
void | deleteOnExit() | 根据File对象,删除该文件,在程序运行结束后删除 |
boolean | mkdir() | 创建File对象代表的目录 |
boolean | mkdirs() | 创建File对象代表的目录,若存在中间目录,一起创建 |
String[] | list() | 返回File对象代表的目录下的所有文件名 |
File[] | listFiles() | 返回File对象代表的目录下的所有文件,以File类型表示 |
boolean | canRead() | 判断用户是否对文件具有读权限 |
boolean | canWrite() | 判断用户是否对文件具有写权限 |
boolean | renameTo(File dest) | 进行文件改名,也可以视为剪切、粘贴操作 |
1.2 代码示例
示例1:
观察各个get方法的特点与差异
/**
* 测试一系列get方法(绝对路径)
*/
public class TestGetMethod {
public static void main(String[] args) throws IOException {
File file = new File("D:/test1/test.txt");
// 1. getParent方法
System.out.println(file.getParent());
// 2. getName方法
System.out.println(file.getName());
// 3. getPath方法
System.out.println(file.getPath());
// 4. getAbsolutePath方法
System.out.println(file.getAbsolutePath());
// 5. getCanonicalPath方法
System.out.println(file.getCanonicalPath());
}
}
运行结果:
可以发现,如果使用 绝对路径 作为File构造方法的参数,那么无论是getPath、getAbsolutePath、getCanonicalPath结果均一致,但是如果使用相对路径那么结果就不同了!
public class TestGetMethod02 {
public static void main(String[] args) throws IOException {
File file = new File("./test.txt");
// 1. getParent方法
System.out.println(file.getParent());
// 2. getName方法
System.out.println(file.getName());
// 3. getPath方法
System.out.println(file.getPath());
// 4. getAbsolutePath方法
System.out.println(file.getAbsolutePath());
// 5. getCanonicalPath方法
System.out.println(file.getCanonicalPath());
}
}
运行结果:
示例2:
观察文件是否存在与判断类型方法
public class TestFileTypeAndExistsMethod {
public static void main(String[] args) {
File file = new File("./test.txt");
// 1. exists方法
System.out.println(file.exists());
// 2. isFile方法
System.out.println(file.isFile());
// 3. isDirectory方法
System.out.println(file.isDirectory());
}
}
当我们在IDEA中创建的工作目录下新建一个test.txt
文本文件,然后执行上述方法,我们可以得知该文件真实存在并且是普通文件而不是目录类型。
运行结果:
示例3:
观察普通文件的创建、删除方法
/**
* 测试普通文件的新建与删除
*/
public class FileCreateAndDelete {
public static void main(String[] args) throws IOException {
// 1. 创建文件实例
File file = new File("./test.txt"); // 这里要求对应物理文件不存在
// 2. 判断对应物理文件是否存在
boolean isExists = file.exists();
System.out.println(isExists);
// 3. 如果不存在则新建文件
if (!isExists) {
boolean createRes = file.createNewFile(); // 新建文件
System.out.println(createRes); // 打印创建结果
}
// 4. 删除对应文件
boolean deleteRes = file.delete();
System.out.println(deleteRes); // 打印删除结果
}
}
运行结果:
调用exists
方法返回false,说明相对路径表示的./test.txt
在物理空间上并不存在,调用createNewFile
方法可以创建该文件,返回true说明创建成功!然后调用delete
方法删除了该文件,删除成功则返回true
补充:如果我们使用 deleteOnExit() 方法进行删除,表示在进程结束后才会将文件进行删除,常用于临时文件,例如在日常生活中打开Word文档时会有一个$开头的临时文件出现,该文件用于对Word文档实时保存!当关闭Word文档后该文档也会随之关闭!
示例4:
观察目录的创建方法
/**
* 目录文件的创建
*/
public class DirectoryCreate {
public static void main(String[] args) {
// 1. 创建目录File实例
File file = new File("./aa"); // 这里要求物理目录不存在
// 2. 判断是否存在
boolean isExists = file.exists();
System.out.println(isExists);
// 3. 调用mkdir方法创建目录
if (!isExists) {
boolean createRes = file.mkdir();
System.out.println(createRes);
}
}
}
运行结果:
当创建该File实例时,调用exists
方法返回false,说明对应物理目录不存在,使用mkdir
方法创建对应目录,返回true,说明创建成功!
注意:这里mkdir方法只能创建一级目录,如果创建的目录中间包含多级中间目录且中间目录均不存在时,必须使用mkdirs方法把中间目录一起创建!
示例5:
观察listFiles和list方法
我们首先在当前项目的工作目录下创建文件夹test,然后在test文件夹上创建子文件夹test1与文件file1.txt,文件结构如下:
/**
* 测试listFiles与list方法
*/
public class ListFilesAndListMethod {
public static void main(String[] args) {
// 1. 创建目录对应File实例
File file = new File("./test");
// 2. 判断file是否为目录
boolean isDirectoryRes = file.isDirectory();
System.out.println(isDirectoryRes);
// 3. 调用list方法
String[] childFilesNames = file.list();
System.out.println(Arrays.toString(childFilesNames));
System.out.println("===============================");
// 4. 调用listFiles方法
File[] childFiles = file.listFiles();
System.out.println(Arrays.toString(childFiles));
}
}
运行结果:
可以发现list
方法列出当前目录下的所有文件名,返回字符串数组,listFiles
方法用户列出当前目录下面所有文件,返回File类型数组
2. 对文件内容的操作
**文件流:**OS提供了流的概念,Java标准库对流进行了封装后形成了一系列类用于文件的读写操作。Java的文件流可以分为两类:1、字节流;2、字符流
**字节流:**以字节作为传输数据的最小单位,代表类有InputStream、OutputStream
**字符流:**以字符作为传输数据的最小单位,一个字符可以包含多个字节,例如在UTF-8编码中1个中文字符占3个字节,代表类有Reader、Writer
2.1 字节输入流与输出流
Java提供了类InputStream与OutputStream用于以字节作为最小数据传输单元的流,InputStream与OutputStream都是抽象类,无法实例化对象,因此需要实例化其子类FileInputStream、FileOutputStream对文件进行读写操作。
FileInputStream读取数据:
返回类型 | 签名 | 说明 |
---|---|---|
int | read() | 读取一个字节内容并返回,如果读取结束返回-1 |
int | read(byte[] b) | 最多读取len字节内容并写入b中,返回实际读取个数,读取结束返回-1 |
int | read(byte[] b, int off, int len) | 最多读取len字节内容到b中,从off偏移位置开始,返回实际读取的个数,读取结束返回-1 |
代码示例:
在编写代码之前,我们在当前工作目录下创建路径为./test/test.txt
的文件,并写入内容abcdef
用于测试读取。
/**
* 使用FileInputStream读取数据
*/
public class FileInputStreamExp {
public static void main(String[] args) {
// 1. 创建FileInputStream
try(InputStream inputStream = new FileInputStream("./test/test.txt")) {
// 2. 使用read方法循环读取一个字节
int val = 0;
while ((val = inputStream.read()) != -1) {
System.out.printf("%x ", val); // 十六进制表示
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
上述结果正是abcdef对应ASCII码值的十六进制表示形式。
但是循环读取单个字节在数据量庞大的时候效率低下,我们使用 输出型参数 的的byte[]数组可以一次性读入多个字节存放到字节数组中,下面是代码示例:
/**
* 使用输出型参数byte[] 一次性读取多个字节
*/
public class FileInputStream02 {
public static void main(String[] args) {
// 1. 创建FileInputStream实例
try(InputStream inputStream = new FileInputStream("./test/test.txt")) {
// 2. 使用字节数组读取
byte[] buffer = new byte[1024];
int n = inputStream.read(buffer);
// 将前n个字节转化为String并打印
String str = new String(buffer, 0, n);
System.out.println(str);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
我们从文件中一次性读取多个字节写入字节数组buffer
中,然后获取前n个字节内容(n为实际读取的字节个数)转化为String类型并打印,得到结果abcdef
FileOutputStream写入数据:
返回类型 | 签名 | 说明 |
---|---|---|
void | write(int b) | 向文件中写入一个字节内容 |
void | write(byte[] b) | 将字节数组中b.length长度内容写入文件 |
void | write(byte[] b, int off, int len) | 将字节数组中从偏移量off开始len长度内容写入文件 |
代码示例:
在编写代码之前,我们在当前工作目录下创建路径为./test/testWrite.txt
的空文件,用来测试写文件操作
/**
* 测试写入一个字节
*/
public class TestWrite {
public static void main(String[] args) {
// 1. 打开文件
try(FileOutputStream fos = new FileOutputStream("./test/testWrite.txt")) {
// 2. 写入一个字节内容
byte b = 97;
fos.write(b);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
执行上述代码后我们会发现./test/testWrite.txt
文件中出现了a,证明写入成功!下面我们测试以字节数组作为参数的write方法。
/**
* 测试写入整个字节数组
*/
public class TestWriteByteArr {
public static void main(String[] args) {
// 1. 打开文件
try(FileOutputStream fos = new FileOutputStream("./test/testWrite.txt")) {
// 2. 将字符串"hello world"变为字节数组
byte[] content = "hello world".getBytes();
// 3. 写入文件
fos.write(content);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
此时我们发现./test/testWrite.txt
中的内容已经变为了"hello world",可以证明写入字节数组内容成功!但是我们还发现一个问题,原来的a怎么不见了,难道Java写文件的时候是覆盖写的么?
补充:Java中若使用文件输出流,那么在打开文件的时候,就会清空其中的内容,那么我们如何做到向文件追加写入呢?只需要在打开文件的时候传递参数true即可,例如FileOutputStream fos = new FileOutputStream(“./test/testWrite.txt”, true)就可以实现追加写了。
2.2 字符输入流与输出流
同样的,Java提供Reader与Writer类用于字符流数据的传输,它们同样都是抽象类,我们需要实例化其子类FileReader、FileWriter对象用于对文件进行读写操作。
FileReader读取数据:
返回类型 | 签名 | 说明 |
---|---|---|
int | read() | 从文件中读取一个字符内容,读取完毕返回-1 |
int | read(char[] cbuf) | 从文件中读取若干字符到字符数组中,返回实际读取长度,读取完毕返回-1 |
int | read(char[] cbuf, int off, int len) | 从文件中读取若干字符到字符数组中,从偏移位置off开始,返回实际读取长度,读取完毕返回-1 |
代码示例:
在编写代码之前,我们在当前工作目录下创建路径为./test/testFileReaderRead.txt
的文件,并写入内容abcdef
用来测试读取方法
/**
* 使用FileReader读取一个字符
*/
public class FileReader01 {
public static void main(String[] args) {
// 1. 打开文件
try(FileReader fr = new FileReader("./test/testFileReaderRead.txt")) {
int ret = 0;
while ((ret = fr.read()) != -1) {
// 2. 循环读取1个字符并打印
System.out.printf("%c\n", ret);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
我们循环读取文件中的内容,并返回读取的单个字符,我们将其进行打印,直到-1结束循环,运行结果符合预期!接下来我们尝试一次性读取多个字符
/**
* 一次性读取多个字符
*/
public class FileReader02 {
public static void main(String[] args) {
// 1. 打开文件
try(FileReader fr = new FileReader("./test/testFileReaderRead.txt")) {
// 2. 循环读取多个字符内容
int len = 0;
char[] cbuff = new char[1024];
while ((len = fr.read(cbuff)) != -1) {
// 打印
System.out.println(new String(cbuff, 0, len));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
FileWriter写入数据:
返回类型 | 签名 | 说明 |
---|---|---|
void | write(int c) | 写入一个字符数据 |
void | write(char[] cbuf) | 写入一个字符数组数据 |
void | write(char[] cbuf, int off, int len) | 写入字符数组部分内容,从偏移量off开始,写入长度为len |
void | write(String str) | 写入字符串数据 |
void | write(String str, int off, int len) | 写入字符串部分内容,从偏移量off开始,写入长度为len |
代码示例:
在编写代码之前,我们在当前工作目录下创建路径为./test/testFileWriterWrite.txt
的空文件,并使用上述write
方法尝试向该文件中写入内容,虽然重载的方法有很多,但是最常用的还是write(String str)
方法
/**
* 测试使用FileWriter写入字符串
*/
public class TestFileWriter {
public static void main(String[] args) {
// 1. 打开文件
try(FileWriter fw = new FileWriter("./test/testFileWriterWrite.txt")) {
// 2. 写入字符串
fw.write("这是一段中文。。。");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
3. 文件操作案例
我们已经学会了如何使用基本的文件操作API,那么接下来让我们写几个小案例来熟练一下叭!
3.1 普通文件的复制
要求:由用户输入原普通文件的路径(原文件必须存在),然后让用户输入需要复制的目标文件路径(目标文件可以不存在)
** 参考代码:**
/**
* 由用户输入原普通文件的路径(原文件必须存在),然后让用户输入需要复制的目标文件路径(目标文件可以不存在)
*/
public class FileCopy {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
// 1. 用户输入原文件路径
System.out.println("请输入原文件路径:");
String srcPath = scanner.next();
// 2,判断原文件合法性
File srcFile = new File(srcPath);
if (!srcFile.exists()) {
System.out.println("原文件不存在!");
return;
}
if (!srcFile.isFile()) {
System.out.println("原文件不是普通文件!");
return;
}
// 3. 用户输入目标文件路径
System.out.println("请输入目标文件路径:");
String dstPath = scanner.next();
File dstFile = new File(dstPath);
// 4. 合法性判断
if (!dstFile.getParentFile().exists()) {
System.out.println("目标路径不合法!");
}
if (!dstFile.getParentFile().isDirectory()) {
System.out.println("目标路径不合法!");
}
// 5. 目标路径不存在就创建普通文件
if (!dstFile.exists()) {
boolean createRes = dstFile.createNewFile();
if (!createRes) {
System.out.println("文件创建失败!");
return;
}
}
// 6. 文件复制
fileCopy(srcFile, dstFile);
}
/**
* 文件拷贝方法
* @param srcFile
* @param dstFile
* @return
*/
private static void fileCopy(File srcFile, File dstFile) {
try(FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(dstFile)) {
int len = 0;
byte[] buffer = new byte[1024];
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
3.2 递归扫描
要求:扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下实验
参考代码:
/**
* 要求:扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
*/
public class FileScan {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 1. 用户输入扫描目录
System.out.println("请输入想要扫描的目录路径:");
String dirPath = scanner.next();
File dirFile = new File(dirPath);
// 2. 判断合法性
if (!dirFile.exists()) {
System.out.println("该目录不存在!");
return;
}
if (!dirFile.isDirectory()) {
System.out.println("该目录不合法!");
return;
}
// 3. 用户输入查找内容
System.out.println("请输入查找内容:");
String searchContent = scanner.next();
// 4. 递归查找
findDFS(dirFile, searchContent);
}
private static void findDFS(File dir, String content) {
// 列出当前目录所有文件
File[] childFiles = dir.listFiles();
for (File childFile : childFiles) {
if (childFile.isFile()) {
// 如果是普通文件
try(FileInputStream fis = new FileInputStream(childFile)) {
StringBuilder sBuilder = new StringBuilder();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = fis.read(buffer)) != -1) {
// 循环读取
sBuilder.append(new String(buffer, 0, len));
}
// 比较内容
if (sBuilder.indexOf(content) != -1) {
// 说明存在
System.out.println("查找到相关内容,在文件" + childFile.getCanonicalPath() + "中");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
// 说明是目录递归查找
findDFS(childFile, content);
}
}
}
}
3.3 递归删除
要求:扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
参考代码:
/**
* 要求:扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
*/
public class FileDelete {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 1. 用户输入扫描目录
System.out.println("请输入需要扫描的目录路径:");
String scanDirPath = scanner.next();
File scanDirFile = new File(scanDirPath);
// 2. 检验合法性
if (!scanDirFile.exists()) {
System.out.println("该目录不存在!");
return;
}
if (!scanDirFile.isDirectory()) {
System.out.println("该目录不合法!");
return;
}
// 3. 由用户输入查找字符
System.out.println("请输入查找字符:");
String searchContent = scanner.next();
// 4. 递归查找
searchDFS(scanDirFile, searchContent);
}
private static void searchDFS(File dir, String content) {
// 列出所有子级文件
File[] childFiles = dir.listFiles();
for (File childFile : childFiles) {
if (childFile.isDirectory()) {
// 是目录继续递归
searchDFS(childFile, content);
} else {
// 普通文件就获取所有内容
try(FileInputStream fis = new FileInputStream(childFile)) {
StringBuilder sBuilder = new StringBuilder();
int len = 0;
byte[] buffer = new byte[1024];
while((len = fis.read(buffer)) != -1) {
sBuilder.append(new String(buffer, 0, len));
}
// 查找
if (sBuilder.indexOf(content) != -1) {
System.out.println("找到了目标文件:" + childFile.getCanonicalPath());
// 是否确认删除
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("是否删除?(Y/N)");
String answer = scanner.next();
if (answer.equalsIgnoreCase("Y")) {
// 进行删除
childFile.deleteOnExit();
break;
} else if (answer.equalsIgnoreCase("N")) {
// 不进行删除
break;
} else {
// 输入有误
System.out.println("输入有误,请重新输入!");
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}