一、认识文件
文件 是存储在硬盘上的!
硬盘和内存的区别:
- 内存存储空间小,硬盘空间大.
- 内存访问速度快,硬盘速度慢.
- 内存成本高,硬盘便宜.
- 内存掉电数据丢失,硬盘掉电数据还在.
文件也是被操作系统管理的. 操作系统内核中有一个专门的模块,文件系统.
Java 针对文件系统/文件进行了一系列的封装.
1、树型结构组织 和 目录
同时,随着文件越来越多,对文件的系统管理也被提上了日程,如何进行文件的组织呢,一种合乎自然的想法出现了,就是按照层级结构进行组织 —— 也就是我们数据结构中学习过的树形结构。这样,一种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹(folder)或者目录(directory)的概念。
2、文件路径(Path)
如何在文件系统中如何定位我们的一个唯一的文件就成为当前要解决的问题,但这难不倒计算机科学家,因为从树型结构的角度来看,树中的每个结点都可以被一条从根开始,一直到达的结点的路径所描述,而这种描述方式就被称为文件的绝对路径(absolute path)。
除了可以从根开始进行路径的描述,我们可以从任意结点出发,进行路径的描述,而这种描述方式就被称为相对路径(relative path),相对于当前所在结点的一条路径。
3、其他知识
即使是普通文件,根据其保存数据的不同,也经常被分为不同的类型,我们一般简单的划分为文本文件和二进制文件,分别指代保存被字符集编码的文本和按照标准格式保存的非被字符集编码过的文件。
Windows 操作系统上,会按照文件名中的后缀来确定文件类型以及该类型文件的默认打开程序。但这个习俗并不是通用的,在 OSX、Unix、Linux 等操作系统上,就没有这样的习惯,一般不对文件类型做如此精确地分类。
文件由于被操作系统进行了管理,所以根据不同的用户,会赋予用户不同的对待该文件的权限,一般地可以认为有可读、可写、可执行权限。
Windows 操作系统上,还有一类文件比较特殊,就是平时我们看到的快捷方式(shortcut),这种文件只是对真实文件的一种引用而已。其他操作系统上也有类似的概念,例如,软链接(soft link)等。
最后,很多操作系统为了实现接口的统一性,将所有的 I/O 设备都抽象成了文件的概念,使用这一理念最为知名的就是 Unix、Linux 操作系统 —— 万物皆文件。
二、Java 中操作文件
Java 中通过 java.io.File 类来对一个文件(包括目录)进行抽象的描述。注意,有 File 对象,并不代表真实存在该文件。
1、File 概述
① 属性
修饰符及类型 | 属性 | 说明 |
---|---|---|
static String | pathSeparator | 依赖于系统的路径分隔符,String 类型的表示 |
static char | pathSeparator | 依赖于系统的路径分隔符,char 类型的表示 |
② 构造方法
签名 | 说明 |
---|---|
File(File parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径 |
File(String parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
③ 方法
修饰符及返回值类型 | 方法签名 | 说明 |
---|---|---|
String | getParent() | 返回 File 对象的父目录文件路径 |
String | getName() | 返回 FIle 对象的纯文件名称 |
String | getPath() | 返回 File 对象的文件路径 |
String | getAbsolutePath() | 返回 File 对象的绝对路径 |
String | getCanonicalPath() | 返回 File 对象的修饰过的绝对路径 |
boolean | exists() | 判断 File 对象描述的文件是否真实存在 |
boolean | isDirectory() | 判断 File 对象代表的文件是否是一个目录 |
boolean | isFile() | 判断 File 对象代表的文件是否是一个普通文件 |
boolean | createNewFile() | 根据 File 对象,自动创建一个空文件。成功创建后返回 true |
boolean | delete() | 根据 File 对象,删除该文件。成功删除后返回 true |
void | deleteOnExit() | 根据 File 对象,标注文件将被删除,删除动作会到 JVM 运行结束时才会进行 |
String[] | list() | 返回 File 对象代表的目录下的所有文件名 |
File[] | listFiles() | 返回 File 对象代表的目录下的所有文件,以 File 对象表示 |
boolean | mkdir() | 创建 File 对象代表的目录 |
boolean | mkdirs() | 创建 File 对象代表的目录,如果必要,会创建中间目录 |
boolean | renameTo(File dest) | 进行文件改名,也可以视为我们平时的剪切、粘贴操作 |
boolean | canRead() | 判断用户是否对文件有可读权限 |
boolean | canWrite() | 判断用户是否对文件有可写权限 |
2、代码示例
① 观察 get 系列的特点和差异
package file;
import java.io.File;
import java.io.IOException;
// 演示 File 类的一些常见用法
public class Demo1 {
public static void main(String[] args) throws IOException {
// File file = new File("d:/test.txt");
File file = new File("./test.txt");
// 父目录文件路径
System.out.println(file.getParent());
// 纯文件名称
System.out.println(file.getName());
// 文件路径
System.out.println(file.getPath());
// 绝对路径
System.out.println(file.getAbsolutePath());
// 修饰过的绝对路径
System.out.println(file.getCanonicalPath());
}
}
② 普通文件的创建
package file;
import java.io.File;
import java.io.IOException;
public class Demo2 {
public static void main(String[] args) throws IOException {
// 如果前面没写 ./ ,也相当于是 ./ ,可省略
File file=new File("helloworld.txt");
// 文件是否存在
System.out.println(file.exists());
// 是否为目录
System.out.println(file.isDirectory());
// 是否为文件
System.out.println(file.isFile());
System.out.println("===========================");
// 创建文件
file.createNewFile();
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
System.out.println("===========================");
// 删除文件
file.delete();
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
}
}
③ 普通文件的删除
package file;
import java.io.File;
import java.io.IOException;
public class Demo3 {
public static void main(String[] args) throws IOException, InterruptedException {
// 文件删除
File file = new File("helloworld.txt");
System.out.println(file.exists());
file.createNewFile();
System.out.println(file.exists());
// file.delete();
// 程序退出时才删除
file.deleteOnExit();
Thread.sleep(5000);
System.out.println(file.exists());
}
}
④ 观察目录的创建1
package file;
import java.io.File;
public class Demo4 {
// 创建目录
public static void main(String[] args) {
File file =new File("test");
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println("===========================");
file.mkdir();
System.out.println(file.exists());
System.out.println(file.isDirectory());
}
}
⑤ 观察目录的创建2
package file;
import java.io.File;
public class Demo4 {
// 创建目录
public static void main(String[] args) {
File file =new File("test/aa/1");
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println("===========================");
file.mkdir();
System.out.println(file.exists());
System.out.println(file.isDirectory());
}
}
可以看到当文件路径为多级目录,且中间目录不存在时,mkdir() 就无法创建,此时就需要用到 mkdirs().
package file;
import java.io.File;
public class Demo4 {
// 创建目录
public static void main(String[] args) {
File file =new File("test/aa/1");
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println("===========================");
// file.mkdir();
// 创建多级目录
file.mkdirs();
System.out.println(file.exists());
System.out.println(file.isDirectory());
}
}
⑥ 观察文件重命名
package file;
import java.io.File;
public class Demo5 {
// 文件重命名
public static void main(String[] args) {
File file1 = new File("./test1.txt");
File file2 = new File("./test2.txt");
file1.renameTo(file2);
}
}
三、文件内容的读写 —— 数据流
1、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 这一类 “遥控器” 在计算机中,把他们统称为 句柄(Handler).
为什么要关闭文件:
一个进程,会使用 PCB 来描述(只考虑单线程的进程). PCB 包含很多属性.
其中有一个属性,就是文件描述符表. 这个表是个数组(顺序表),每个元素都对应着当前进程打开的文件.
这个数组的下标,就称为 “文件描述符”.
- 每次打开文件,都会在文件符表中占据一个位置.
- 每次关闭文件,都会释放一个位置.
文件描述符,是存在上限的.
如果一个进程,一直在打开文件,没有释放,此时就会导致我们的进程在进行后续打开的时候,就打开文件失败!!!
2、FileInputStream 概述
① 构造方法
签名 | 说明 |
---|---|
FileInputStream(File file) | 利用 File 构造文件输入流 |
FileInputStream(String name) | 利用文件路径构造文件输入流 |
3、代码示例
① InputStream 完全读文件
package file;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
// 需要先在项目目录下准备好一个 test2.txt 的文件,里面填充 "hello" 的内容
public class Demo6 {
public static void main(String[] args) throws IOException {
// 使用一下 InputStream
// 1. 打开文件
InputStream inputStream = new FileInputStream("test2.txt");
// 2. 读取文件
while (true) {
int b = inputStream.read();
if (b == -1) {
// 文件读完了
break;
}
System.out.println(b);
}
// 3. 关闭文件
inputStream.close();
}
}
② InputStream 填充中文读取
package file;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
// 需要先在项目目录下准备好一个 test2.txt 的文件,里面填充 "hello" 的内容
public class Demo6 {
public static void main(String[] args) throws IOException {
// 使用一下 InputStream
// 1. 打开文件
InputStream inputStream = new FileInputStream("test2.txt");
// 2. 读取文件
byte[] b = new byte[1024];
int len = inputStream.read(b);
// System.out.println(len);
// for (int i = 0; i < len; i++) {
// System.out.println(b[i]);
// }
String s = new String(b, 0, len, "utf8");
System.out.println(s);
// 3. 关闭文件
inputStream.close();
}
}
③ 字符流读取文件
package file;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
// 需要先在项目目录下准备好一个 test2.txt 的文件,里面填充 "你好" 的内容
public class Demo7 {
// 使用字符流读一下文件
public static void main(String[] args) throws IOException {
Reader reader = new FileReader("test2.txt");
char[] buffer = new char[1024];
int len = reader.read(buffer);
for (int i = 0; i < len; i++) {
System.out.println(buffer[i]);
}
reader.close();
}
}
④ try with resources
前面几个例子我们采用的是抛出异常的方式,如果我们想要使用 try/catch 来捕捉异常,代码就会显得很繁琐!
以 例1:InputStream 完全读文件 为例:
package file;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
// 需要先在项目目录下准备好一个 test2.txt 的文件,里面填充 "hello" 的内容
public class Demo6 {
public static void main(String[] args) {
// 使用一下 InputStream
InputStream inputStream = null;
try {
// 1. 打开文件
inputStream = new FileInputStream("test2.txt");
// 2. 读取文件
while (true) {
int b = inputStream.read();
if (b == -1) {
// 文件读完了
break;
}
System.out.println(b);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3. 关闭文件
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这样我们就进行了 try/catch 的嵌套使用,代码看起来就很繁琐,所以我们想到了在 异常 时学的一个思路 try with resources.
package file;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class Demo9 {
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("test2.txt")) {
// 读文件
byte[] b = new byte[1024];
int len = inputStream.read(b);
// ........
} catch (IOException e) {
e.printStackTrace();
}
}
}
这种写法的好处不仅仅是使得代码简洁了,同时,在 try 执行结束后,会自动调用 inputSteam 的 close 方法进行关闭文件. 要求是这个类必须实现 Closeable 接口!
4、利用 Scanner 进行字符读取
构造方法 | 说明 |
---|---|
Scanner(InputStream is, String charset) | 使用 charset 字符集进行 is 的扫描读取 |
① Scanner 获取文本文件
package file;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
// 需要先在项目目录下准备好一个 test2.txt 的文件,里面填充 "你好" 的内容
public class Demo8 {
// 对于文本文件,还有更简单的写法
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("test2.txt");
Scanner scanner = new Scanner(inputStream);
String s = scanner.next();
System.out.println(s);
inputStream.close();
}
}
5、OutputStream 概述
① 方法
修饰符及返回值类型 | 方法签名 | 说明 |
---|---|---|
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(刷新)操作,将数据刷到设备中。 |
② 说明
OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream
6、利用 OutputStreamWriter 进行字符写入
① 直接写入字符
package file;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class Demo10 {
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("test2.txt")) {
outputStream.write('h');
outputStream.write('e');
outputStream.write('l');
outputStream.write('l');
outputStream.write('o');
} catch (IOException e) {
e.printStackTrace();
}
}
}
② 字符数组写入
package file;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class Demo10 {
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("test2.txt")) {
// outputStream.write('h');
// outputStream.write('e');
// outputStream.write('l');
// outputStream.write('l');
// outputStream.write('o');
byte[] b = new byte[]{
(byte) 'h', (byte) 'e', (byte) 'l', (byte) 'l', (byte) 'o'
};
outputStream.write(b);
} catch (IOException e) {
e.printStackTrace();
}
}
}
③ 字符串写入
package file;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class Demo10 {
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("test2.txt")) {
// outputStream.write('h');
// outputStream.write('e');
// outputStream.write('l');
// outputStream.write('l');
// outputStream.write('o');
// byte[] b = new byte[]{
// (byte) 'h', (byte) 'e', (byte) 'l', (byte) 'l', (byte) 'o'
// };
// outputStream.write(b);
String s = "hello world";
outputStream.write(s.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
7、利用 PrintWriter 找到我们熟悉的方法
上述,我们其实已经完成输出工作,但总是有所不方便,我们接来下将 OutputStream 处理下,使用
PrintWriter 类来完成输出,因为 PrintWriter 类中提供了我们熟悉的 print/println/printf 方法
① Writer
package file;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class Demo11 {
public static void main(String[] args) {
try (Writer writer = new FileWriter("test2.txt")) {
writer.write("hello word");
} catch (IOException e) {
e.printStackTrace();
}
}
}
② PrintWriter
package file;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
public class Demo12 {
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("test2.txt")) {
// 此处的 PrintWriter 的用法 和 System.out 是一样的
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println("hello");
printWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、小程序练习
1、文件查找
扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
package file;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
public class Demo13 {
// 实现一个递归遍历文件,并询问删除.
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要扫描的路径:");
String rootPath = scanner.next();
File root = new File(rootPath);
if (!root.exists()) {
System.out.println("您输入的路径不存在,无法进行扫描!");
return;
}
System.out.println("请输入要删除的文件名(或者一部分):");
String toDelete = scanner.next();
// 准备进行递归,通过递归的方式,来找到所有的文件.
// 找到所有的文件之后,再尝试进行删除
scanDir(root, toDelete);
}
public static void scanDir(File rootDir, String toDelete) {
// 加上个日志,看一下这里当前递归的过程
try {
System.out.println(rootDir.getCanonicalPath());
} catch (IOException e) {
e.printStackTrace();
}
File[] files = rootDir.listFiles();
if (files == null) {
// 空目录,直接返回
return;
}
for (File f : files) {
if (f.isDirectory()) {
// 是目录,就进行递归
scanDir(f, toDelete);
} else {
// 普通文件
tryDelete(f, toDelete);
}
}
}
public static void tryDelete(File f, String toDelete) {
// 看看当前文件名是否包含了 toDelete,如果包含,就删除,否则就啥也不干
if (f.getName().contains(toDelete)) {
try {
System.out.println("是否要删除文件(Y/N):" + f.getCanonicalPath());
Scanner scanner = new Scanner(System.in);
String choice = scanner.next();
if (choice.equals("Y")) {
f.delete();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、文件复制
进行普通文件的复制
package file;
import java.io.*;
import java.util.Scanner;
public class Demo14 {
// 实现复制文件的功能
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要复制的文件路径:");
String srcPath = scanner.next();
File srcFile = new File(srcPath);
if (!srcFile.exists()) {
System.out.println("要复制的文件不存在!");
return;
}
if (!srcFile.isFile()) {
System.out.println("要复制的不是普通文件!");
return;
}
System.out.println("请输入要复制到的目标路径:");
String destPath = scanner.next();
File destFile = new File(destPath);
if (destFile.exists()) {
System.out.println("要复制到的目标已经存在!");
return;
}
// -------------------------------------------------------
// 进行拷贝
// -------------------------------------------------------
try (InputStream inputStream = new FileInputStream(srcFile)) {
try (OutputStream outputStream = new FileOutputStream(destFile)) {
byte[] buf = new byte[1024];
while (true) {
int len = inputStream.read(buf);
if (len == -1) {
// 拷贝完成
break;
}
outputStream.write(buf, 0, len);
}
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("复制完成!");
}
}
3、精确查找
扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下实验
package file;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class Demo15 {
// 编码目录,看某个输入的词是否在文件名或者文件内容中存在.
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的目录:");
String rootPath = scanner.next();
File rootFile = new File(rootPath);
if (!rootFile.exists()) {
System.out.println("要扫描的目录不存在!");
return;
}
if (!rootFile.isDirectory()) {
System.out.println("要扫描的路径不是目录");
return;
}
System.out.println("请输入要搜索的词:");
String toFind = scanner.next();
// 递归遍历目录
scanDir(rootFile, toFind);
}
private static void scanDir(File rootFile, String toFind) throws IOException {
File[] files = rootFile.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isDirectory()) {
scanDir(f, toFind);
} else {
tryFindInFile(f, toFind);
}
}
}
// 判断 toFind 是否是文件名 或者 是文件内容的一部分
private static void tryFindInFile(File f, String toFind) throws IOException {
// 是不是文件名的一部分
if (f.getName().contains(toFind)) {
System.out.println("找到了文件名匹配的文件:" + f.getCanonicalPath());
return;
}
// 是不是文件内容的一部分
try (InputStream inputStream = new FileInputStream(f)) {
// 把文件内容整个都读出来
StringBuilder stringBuilder = new StringBuilder();
Scanner scanner = new Scanner(inputStream);
while (scanner.hasNextLine()) {
stringBuilder.append(scanner.nextLine());
}
// 读取完毕了.
if (stringBuilder.indexOf(toFind) >= 0) {
System.out.println("找到了文件内容匹配的文件:" + f.getCanonicalPath());
return;
}
}
}
}
五、代码参考
1、如何按字节进行数据读
try (InputStream is =...) {
byte[] buf = new byte[1024];
while (true) {
int n = is.read(buf);
if (n == -1) {
break;
}
// buf 的 [0, n) 表示读到的数据,按业务进行处理
}
}
2、如何按字节进行数据写
try (OutputStream os = ...) {
byte[] buf = new byte[1024];
while (/* 还有未完成的业务数据 */) {
// 将业务数据填入 buf 中,长度为 n
int n = ...;
os.write(buf, 0, n);
}
os.flush(); // 进行数据刷新操作
}
3、如何按字符进行数据读
try (InputStream is = ...) {
try (Scanner scanner = new Scanner(is, "UTF-8")) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
// 根据 line 做业务处理
}
}
}
4、如何按字符进行数据写
try (OutputStream os = ...) {
try (OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8")) {
try (PrintWriter writer = new PrintWriter(osWriter)) {
while (/* 还有未完成的业务数据 */) {
writer.println(...);
}
writer.flush(); // 进行数据刷新操作
}
}
}