文章目录
本文将会从基础概念和实操方面讲起,实操主要是文件系统方面的操作(创建,删除,修改等)和文件内容方面的操作,最后再针对一些常用的文件操作进行一个代码练习
一、基础概念
注意,本文主要涉及实操方面,如果想知道更多关于理论部分请点击下方链接
-
什么是文件:一整块有关系的数据,一般存放在硬盘或外部设备中,底层全都是一长串的二进制。已经被操作系统封装,由文件系统管理,我们只要了解对应的API即可
-
文件管理系统:
(1)点击文件的属性,可以查看对应的文件系统。
(2)通过查看磁盘管理,可以看见真实的磁盘,每一个盘都有单独的文件系统(ps.所以我们可以自定义给磁盘划分大小,但是这样极易出错,不建议尝试) -
底层数据结构:数据组织主要是【N叉树】的结构,即“目录 – 文件”的形式
-
路径:为了能够准确查找文件,每个文件都有唯一的路径,不过路径的描述方法不同,主要是 “绝对路径” 和 “相对路径” 这两种。
- 绝对路径:以盘符开头
- 相对路径:看基准目录,【.】表示当前所在目录,【…】表示当前目录的上一级
-
文件分类:文件主要分为文本文件和二进制文件两种,当然,文件的底层都是一长串二进制数据。我们可以靠该文件能否被记事本正常打开判断类别,能被正常打开的是文本文件,反之二进制文件。因为记事本的原理,就是尝试把当前数据放在码表中查看。
- 文本文件:存在是字符,字符集编码后的文本
- 二进制文件:一长串二进制,按照标准格式保存的非被字符集编码过的文件
二、文件系统操作(java代码操作)
系统操作有很多,比如创建删除文件,创建目录等,下面将会从这三个方面介绍如何通过java代码实现
2.1 前置知识
为什么不用C语言写:C语言标准库不支持文件系统操作
如何通过 java 代码实现:依靠Java.io.File类,每一个File类都对应着一个具体文件(无论这个文件存在与否都可以对应)
- io:指的是 input(输入) 和 output(输出)。
注意,这个是要站在CPU的角度看待,内存相比硬盘离CPU更近,所以【从硬盘读取数据到内存的行为】是“读取”,从【从内存中读取数据然后写到硬盘上的行为】是“写入”
2.2 File类
属性
修饰符及类型 | 属性 | 说明 |
static String | pathSeparator | 依赖于系统的路径分隔符,String 类型的表示 |
static char | pathSeparator | 依赖于系统的路径分隔符,char 类型的表示 |
方法
点击查看File的方法
2.3 创建文件
public static void createFile() throws IOException {
File file = new File("./","test1.txt");
file.createNewFile();
File file1 = new File("./test2.txt");
file1.createNewFile();
}
注意点
- 关于反斜杠
Linux/Mac 只支持/,但是Windows /(需要写两个,因为要是转义字符) 和 \ 都支持 - 如果文件已经存在了
createNewFile表示如果文件不存在,会按照路径创建一个,如果存在,再调用也不会有什么影响
2.4 删除文件
public static void delete(){
File file = new File("./test1.txt");
file.deleteOnExit();
//file.delete();
}
注意点
- deleteOnExit():当程序正确退出时,删除文件。当程序异常关闭时,不会删除该文件
一般用于临时文件(可以自动用户当前的中间状态,比如office的恢复数据操作)
2.5 创建目录
一次性创建单级目录
public static void createMadir(){
File file = new File("./test");
file.mkdir();
}
一次性创建多级目录
public static void createMadirs(){
File file = new File("./test/first/second");
file.mkdirs();
}
注意点
- mkdir:make directory
- mkdirs:make directories
2.6 文件重命名
要求括号里放的是不存在状态的File类,然后调用方法的要求存在
public static void renameDir() throws IOException {
File file1 = new File("./test/first/test.txt");
File file2 = new File("./test/test3.txt");
file2.createNewFile();
file2.renameTo(file1);
}
三、文件内容操作
文件内容方面的操作则主要依靠文件流,流分为字节流和字符流,其中字节流主要用于二进制文件,字符流主要用于文本文件。下面将会先概述一下通用操作,然后再从字节流和字符流两个方面详细讲解
3.1 通用操作
字节流or字符流?
对于文件内容的操作,Java也提供了一些接口
- 二进制文件:使用字节流,操作字节,InputStream(输入流)、OutputStream(输出流)
- 文本文件:使用字符流,操作字符,Reader(输入流)、Writer(输出流)
ps.
(1)后续的一些其他的类,基本是上述四个抽象类的子类
(2)理论上不管什么文件都可以用字节流打开,只是文本文件用字符流更加方面而已
基本步骤
- 构造方法打开文件
- 【1】如果衍生自 InputStream/Reader,可以用read读数据(从硬盘读到内存,站在CPU角度)
【2】如果衍生自OutPutStream/Writer,可以用write写数据(从内存写到硬盘,站在CPU角度) - close关闭文件
close()
【1】为什么要close
释放必要资源。一个进程打开一个文件,需要像系统申请一定资源(会占用PCB的文件描述符表中的一个表项,而这个文件描述符表就像是个不会自动扩容的顺序表,长度有限,如果不及时关闭,一旦被占满,就无法打开新的文件了)
【2】 如何正确close
- 通过finally,避免遇到抛出异常,return等原因,使得close方法没有执行到的情况
- try with resources:try(),括号里面放资源创建语句
3.2 字节流使用
InputStream
修饰符及返回值类型 | 方法签名 | 说明 |
int | read() | 读取一个字节的数据,返回 -1 代表已经完全读完了 |
int | read(byte[] b) | 最多读取 b.length 字节的数据到 b 中,返回实际读到的数量;-1 代表以及读完了 |
int | read(byte[] b, int off, int len) | 最多读取 len - off 字节的数据到 b 中,放在从 off 开始,返回实际读到的数量;-1 代表以及读完了 |
void | close | 关闭字节流 |
InputStream 只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使用 FileInputStream
FileInputStream
签名 | 说明 |
FileInputStream(File file) | 利用 File 构造文件输入流 |
FileInputStream(String name) | 利用文件路径构造文件输入流 |
代码示例 — 读取
读取英文字符:其实读出来的都是一个个数字,我们需要用 %c 用字符表示出来
public static void readFile() throws IOException {
try(InputStream inputStream = new FileInputStream("./test/first/test.txt")){
while (true){
int b = inputStream.read();
if (b == -1){
break;
}
System.out.printf("%c", b);
}
}
}
public static void readFile1() throws IOException {
try(InputStream inputStream = new FileInputStream("./test/first/test.txt")){
byte[] buf = new byte[1024];
int len;
while (true){
len = inputStream.read(buf);
if (len == -1){
break;
}
for (int i = 0; i < len; i++) {
System.out.printf("%c", buf[i]);
}
}
}
}
读取中文字符
用FileInputStream读取中文字符十分麻烦,因为我们要指定字节。为了提高效率,使用Scanner类进行读取
构造方法 | 说明 |
Scanner(InputStream is, String charset) | 使用 charset 字符集进行 is 的扫描读取 |
public static void readFile2() throws IOException {
try(InputStream inputStream = new FileInputStream("./test/first/test.txt")){
try(Scanner scanner = new Scanner(inputStream,"UTF-8")){
while(scanner.hasNext()){
String s = scanner.next();
System.out.print(s);
}
}
}
}
OutPutStream
输出流对象会在打开文件之后,清空文件内容(不管是字节流还是字符流)。如果想要不清空,就需要用追加写方式,即在构造方法后面加一个true.
try(Writer writer = new FileWriter(“路径”, true)){
}
修饰符及返回值类型 | 方法签名 | 说明 |
Scanner(InputStream is, String charset) | 使用 charset 字符集进行 is 的扫描读取 | 使用 charset 字符集进行 is 的扫描读取 |
void | write(int b) | 写入要给字节的数据 |
void | write(byte[] b) | 将 b 这个字符数组中的数据全部写入 os 中 |
int | write(byte[] b, int off, int len) | 将 b 这个字符数组中从 off 开始的数据写入 os 中,一共写 len 个 |
void | close() | 关闭字节流 |
void | flush() | 重要:我们知道 I/O 的速度是很慢的,所以,大多的 OutputStream 为了减少设备操作的次数,在写数据的时候都会将数据先暂时写入内存的一个指定区域里,直到该区域满了或者其他指定条件时才真正将数据写入设备中,这个区域一般称为缓冲区。但造成一个结果,就是我们写的数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置,调用 flush(刷新)操作,将数据刷到设备中。 |
代码示例 — 写入
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
try (OutputStream os = new FileOutputStream("output.txt")) {
try (OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8")) {
try (PrintWriter writer = new PrintWriter(osWriter)) {
writer.println("我是第一行");
writer.print("我的第二行\r\n");
writer.printf("%d: 我的第三行\r\n", 1 + 1);
writer.flush();
}
}
}
}
}
3.2 字符流使用
和字节流使用方式差不多只不过是类名换了一下而已。
三、练习
3.1 删除指定文件
扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
import java.io.File;
import java.util.Scanner;
public class Solution {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
// 1. 让用户输入一个目录. 后续的查找都是针对这个目录来进行的.
System.out.println("请输入要搜索的根目录: ");
File rootPath = new File(scanner.next());
// 2. 再让用户输入要搜索/要删除的关键词.
System.out.println("请输入要删除的关键词: ");
String word = scanner.next();
// 3. 判定一下当前输入的目录是否有效.
if (!rootPath.isDirectory()) {
System.out.println("您此时输入的路径不是合法目录!");
return;
}
// 4. 遍历目录. 从根目录出发, 按照 深度优先(递归) 的方式, 进行遍历
scanDir(rootPath, word);
}
public static void scanDir(File currentDir, String word) {
// 1. 先列出当前目录中都包含哪些内容.
File[] files = currentDir.listFiles();
if (files == null || files.length == 0) {
// 空的目录或者非法的目录
return;
}
// 2. 遍历列出的文件, 分两个情况分别讨论.
for (File f : files) {
// 加个日志, 方便看程序执行的过程.
System.out.println(f.getAbsolutePath());
if (f.isFile()) {
// 3. 如果当前文件是普通文件, 看看文件名是否包含了 word, 来决定是否要删除.
dealFile(f, word);
} else {
// 4. 如果当前文件是目录文件, 就递归执行 scanDir
scanDir(f, word);
}
}
}
private static void dealFile(File f, String word) {
// 1. 先判定当前文件名是否包含 word
if (!f.getName().contains(word)) {
// 此时这个文件不包含 word 关键词. 直接跳过.
return;
}
// 2. 包含 word 就需要询问用户是否要删除该文件?
System.out.println("该文件是: " + f.getAbsolutePath() + ", 是否要确认删除? (Y/N)");
String choice = scanner.next();
if (choice.equals("Y") || choice.equals("y")) {
f.delete();
}
// 如果是其他值, 都忽略.
}
}
3.2 复制文件
import java.io.*;
import java.util.Scanner;
// 完成文件复制.
public class Solution {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
// 1. 输入路径并且合法性判定
System.out.println("请输入要复制的源文件路径: ");
String src = scanner.next();
File srcFile = new File(src);
if (!srcFile.isFile()) {
System.out.println("您输入的源文件路径非法!");
return;
}
System.out.println("请输入要复制到的目标路径: ");
String dest = scanner.next();
File destFile = new File(dest);
// 不要求目标文件本身存在. 但是得保证目标文件所在的目录, 得是存在的.
// 假设目标文件写作 d:/tmp/cat2.jpg, 就需要保证 d:/tmp 目录是存在的.
if (!destFile.getParentFile().isDirectory()) {
System.out.println("您输入的目标文件路径非法!");
return;
}
// 2. 进行复制操作的过程. 按照字节流打开.
try (InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputStream = new FileOutputStream(destFile)) {
while (true) {
byte[] buffer = new byte[20480];
int n = inputStream.read(buffer);
System.out.println("n = " + n);
if (n == -1) {
System.out.println("读取到 eof, 循环结束. ");
break;
}
outputStream.write(buffer, 0, n);
}
}
}
}
3.3 查找文件
扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
public static void search() throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要查找的根目录:");
File rootPath = new File(scanner.next());
if (!rootPath.isDirectory()){
System.out.println("您输入的根目录有误");
return;
}
System.out.println("请输入要查找的文件名");
String fileName = scanner.next();
List<File> res = new ArrayList<>();
scanDir(rootPath, fileName, res);
System.out.println("共找到了符合条件的文件 " + res.size() + "个,它们分别是" );
for (File file : res) {
System.out.println(file.getCanonicalPath());
}
}
public static void scanDir(File currentDir, String searchName, List<File> res) throws IOException {
File[] files = currentDir.listFiles();
if (files == null || files.length == 0){
return;
}
for (File f : files){
if (f.isDirectory()){
scanDir(f, searchName, res);
}else{
if (isContentContains(f, searchName)) {
res.add(f.getAbsoluteFile());
}
}
}
}
public static boolean isContentContains(File currentFile, String searchName) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
try(InputStream inputStream = new FileInputStream(currentFile)){
try(Scanner scanner = new Scanner(inputStream, "UTF-8")){
while (scanner.hasNextLine()){
stringBuilder.append(scanner.nextLine());
stringBuilder.append("\r\n");
}
}
}
return stringBuilder.indexOf(searchName) != -1;
}