文件是在硬盘上存储数据的方式。操作系统帮我们把硬盘的一些细节都封装起来了。程序猿只需要了解文件的相关接口即可。
硬盘用来存储数据,和内存相比,硬盘的存储空间更大,访问速度更慢,成本更低,持久化存储。
操作系统通过“文件系统”这样的模块来管理硬盘。
实际上我们的电脑上只有一个硬盘,操作系统可以通过文件系统把这个硬盘抽象成多个硬盘一样。
不同的文件系统,管理文件的方式都是类似的。通过目录——文件构成了“N叉树”树形结构。
比如说现在要找 cat 这个图片
此电脑 => D盘 => tmp => cat.jpg ( d:/tmp/cat.jpg )
通过这个路线就能找到/确定电脑上唯一一个文件,这个东西就叫做路径。
windows 上用 / 或者 \ 来分割不同的目录。
比如:d:/tmp/cat.jpg 或者 d:\tmp\cat.jpg
以上述这个目录为例,以盘符开头的路径,可以叫做“绝对路径”,以 . 或者 … 开头的路径,叫做“相对路径”。
相对路径,需要有一个 “基准路径” / “工作目录”,表示从这个基准目录出发,怎么走能找到这个文件。
同样是一个cat.jpg 的文件,站在不同的基准目录上,查找的路径是不相同的!!
文件系统上存储的文件,具体来说又分成两个大类。
文本文件
存储的是字符
二进制文件
存储的是二进制数据
一个最简单的方式来,判断文件是二进制还是文本,就是直接用记事本打开,能看懂的是文本文件,看不懂的则是二进制文件。
后续针对文本的操作,文本和二进制,操作方式是完全不同的。
当我们站在CPU角度上来看待 输入输出 时,如下图:
Java 中通过 java.io.File 类来对一个文件(包括目录)进行抽象的描述。
下面参数的字符串,就表示一个路径。可以是绝对路径,也可以是相对路径。
签名 | 说明 |
---|---|
File(File parent, Stringchild) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径 |
File(String parent, Stringchild) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
windows 上目录之间的分隔符两种都支持,可以使用 / 也可以使用 \ 。而 linux / mac 就只是支持 /
即使在 windows,也尽量使用 / 。如果要使用 \ 则需要在代码中搭配转义字符。
修饰符及类型 | 属性 | 说明 |
---|---|---|
static String | pathSeparator | 依赖于系统的路径分隔符,String 类型的表示 |
static char | pathSeparator | 依赖于系统的路径分隔符,char 类型的表示 |
修饰符及返回值类型 | 方法签名 | 说明 |
---|---|---|
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(Filedest) | 进行文件改名,也可以视为我们平时的剪切、粘贴操作 |
boolean | canRead() | 判断用户是否对文件有可读权限 |
boolean | canWrite() | 判断用户是否对文件有可写权限 |
在 idea 中运行一个程序,工作目录就是项目所在的目录。
import java.io.File;
import java.io.IOException;
public class practice1 {
public static void main(String[] args) throws IOException {
File file = new File("../aaa/test.txt");
System.out.println("get.getPath(): " + file.getPath());
System.out.println("get.getParent(): " + file.getParent());
System.out.println("get.getName(): " + file.getName());
System.out.println("get.getAbsolutePath(): " + file.getAbsolutePath());
//把工作目录 拼接上 当前目录
System.out.println("get.getCanonicalPath(): " + file.getCanonicalPath());
}
}
输出结果为:
import java.io.File;
public class practice2 {
public static void main(String[] args) {
File file = new File("d:/temp/aaa/2222"); //是一个目录
//File file = new File("d:/temp/aaa/2222/test.txt"); //是一个文件
System.out.println(file.isDirectory());
System.out.println(file.isFile());
System.out.println(file.exists());
}
}
mkdir()只能创建一层目录,如果父目录不存在,则创建失败。例如,如果你想在e:/test目录下创建一个名为example的子目录,但是test目录不存在,则创建失败。
而mkdirs()可以创建多层目录,如果父目录不存在,则会一并创建。例如,如果你想在e:/test目录下创建一个名为example的子目录,但是test目录不存在,则mkdirs()方法会先创建test目录,然后再创建example目录。
import java.io.File;
public class practice5 {
public static void main(String[] args) {
File file = new File("./temp11");
File file1 = new File("./src/temp111");
file.renameTo(file1); //可以进行类似于粘贴、剪切的操作
}
}
以上文件系统的操作,都是基于File类来完成的。另外还需要文件内容的操作。我们可以称之为文件流 stream。
当我们想读取100字节文件数据是时,可以一次读完,也可以分两次一次读 50,十次一次读10字节等等…
关于文件内容的读写有一系列类:
后续一些操作字节类/字符的类都是由上述这两个类所衍生出来的。
如果使用 InputStream 或 Reader ,就可以使用 read 方法来读数据
如果使用 OutputStream 或 Writer,就可以使用 write 方法来写数据
对于文件操作来说,打开文件、操作文件、关闭文件都是十分重要的环节。
当我们申请打开一个文件后,是需要从系统这里申请一定的资源的。(占用进程的 pcb 里的文件描述符表中的一个选项,其中这个文件描述符表可以看做为一个顺序表,其长度有限,不会进行自动扩容)
用完文件后如果没有及时关闭释放,就会出现“文件资源泄露”很严重的问题。一旦一直打开文件,而不去关闭不用的文件,文件的描述符表就会被吃满,后续就无法继续打开新的文件了。
这里关闭文件,介绍一种简单的方式,后续文件操作都可以使用——try with resources.
用于自动关闭实现了各种接口的资源,在try语句块中声明资源,当try语句块执行完毕后,会自动关闭这些资源,无需手动调用 close() 方法。try-with-resources语句也可以包含catch和finally语句,用于处理异常和执行必要的清理操作。这种语法的使用可以简化代码,提高代码的可读性和可维护性。
import java.io.*;
public class practice6 {
public static void main(String[] args) throws IOException {
try(Reader reader = new FileReader("d:/test.txt")) { //使用try with resources
while (true) {
char[] buf = new char[1024];
int n = reader.read(buf);
if (n == -1) {
break;
}
for (int i = 0; i < 1 ; i++) {
System.out.print(buf[i]);
}
}
}
}
}
read方法:
read(char[] cbuf)
:会把读到的内容,填充到 cbuf 数组中,一次读一个字符直到把数组填满。此处的参数就相当于“输出型参数”。通过 read 可以把一个空数组填充上内容。(就相当于食堂打饭,要先拿个餐盘去装东西)
read 返回值:int n = reader.read(buf)
这里的返回值 n 代表实际读到的字符个数,如果读完就会返回 -1。(0~65535 正是两个字节的范围)
问:utf8 格式下,一个字符应该是 3 个字节,这里读出来的一个字符是 2 个字节??
java 的 char 类型是使用 unicode 编码的(一个字符两个字节),使用这个方法读一个字符,java 标准库内部会帮我们自动完成转换。程序员们感知不到。
有时候会涉及到很多个小文件,需要读取并合并到一起去,就会用到上述办法read(char[] cbuf)
。
往往读一个比较大的文件,需要循环读取。
FileInputStream 构造方法:
签名 | 说明 |
---|---|
FileInputStream(File file) | 利用 File 构造文件输入流 |
FileInputStream(String name) | 利用文件路径构造文件输入流 |
方法:
修饰符及返回类型 | 方法签名 | 说明 |
---|---|---|
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 () | 关闭字节流 |
注:InputSream只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使用 FileInputStream。
文本文件,也可以使用字节流打开。只不过此时你读到的每个字节,就不是完整的字符了。
InputStream 用法和 Reader 十分相似。
read方法:
read(byte[] b)
:同样是把读到的内容填到数组中,一次读一个字节直到把数组填满。
import java.io.*;
public class practice6 {
public static void main(String[] args) throws IOException {
try(InputStream reader = new FileInputStream("d:/test.txt")) { //文件内容:你好世界
while (true) {
byte[] buf = new byte[1024];
int n = reader.read(buf);
if (n == -1) {
break;
}
System.out.println("n = " + n); //输出:n = 12
for (int i = 0; i < n; i++) {
System.out.printf("%x ",buf[i]); //输出:e4 bd a0 e5 a5 bd e4 b8 96 e7 95 8c
}
String s = new String(buf, 0, n, "utf-8"); //通过String构造方法,把字节转化成字符。
System.out.print(s); //输出:你好世界
}
}
}
}
我们查看 UTF-8
编码:
可以看出来,通过字节流,read会把文本中的每个字节都读出来。此时我们确实把字节都读出来了,但是还需要借助工具,把字节转换成字符——直接使用 String 的构造方法。
但是可以看到,利用 String 构造方法比较麻烦,也不优雅。这里介绍一个新方法——Scanner。
操作系统中,"文件"是一个广义的概念。System.in是一个特殊的文件,对应到"标准输入"普通的硬盘上的文件,也是文件。
Scanner都是一视同仁的。只是把当前读到的字节数据进行转换,不关心这个数据究竟是来自于标准输入,还是来自于文件。
以前学过的Scanner的操作,在这里完全适用。
public class practice6 {
public static void main(String[] args) throws IOException {
try(InputStream reader = new FileInputStream("d:/test.txt")) {
Scanner scanner = new Scanner(reader);
String s1 = scanner.next();
System.out.println(s1); //输出:你好世界
}
}
}
输出使用方法和输入非常相似,关键操作就是 write.
write 之前要打开文件,用完了也需要关闭文件。
输出有一个特别需要注意的点,输出流对象(无论是字符流还是字节流)会在打开文件后,清空文件内容。
但是我们可以按照追加写的方式来打开文件,此时就不会清空内容了。
//后面的 true 就代表可以追加数据 append-true
try(Writer writer = new FileWriter("d:/test.txt",true)){}
OutputStream 使用方法,完全一样。
只不过 write 方法,不能支持字符串参数,只能按照字节,或者字节数组来写入。
Scanner 搭配 InputStream 实现简化代码的效果
PrintWriter 则是搭配 OutputStream
1.扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
import java.io.*;
import java.util.Scanner;
public class practice8 {
public static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) throws IOException {
System.out.println("请输入要查询的根目录:");
File rootPath = new File(scanner.next());
if(!rootPath.isDirectory()) {
System.out.println("输入的根目录格式不正确!!");
return;
}
System.out.println("请输入要删除的文件名:");
String name = scanner.next();
scanDir(rootPath, name);
}
public static void scanDir(File rootPath, String name) {
File[] files = rootPath.listFiles();
if(files == null || files.length == 0){
return;
}
for (File file : files) {
System.out.println(file.getAbsolutePath());
if(file.isFile()) {
deleteFile(file, name);
} else {
scanDir(file, name);
}
}
}
public static void deleteFile(File f, String name) {
if(!f.getName().contains(name)) {
return;
}
System.out.println("该文件是:" + f.getName() + ",是否确认要删除?(Y/N)");
String choice = scanner.next();
if(choice.equals("Y") || choice.equals("y")) {
System.out.println("删除成功!");
f.delete();
}
}
}
2.进行普通文件的复制
import java.io.*;
import java.util.Scanner;
public class practice9 {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入源文件地址:");
File src = new File(scanner.next());
if (!src.isFile()) {
System.out.println("您输入的源文件地址不合法!");
return;
}
System.out.println("请输入目标文件地址:");
File dest = new File(scanner.next());
if (!dest.isFile()) {
System.out.println("您输入的目标文件地址不合法!");
return;
}
try (InputStream inputStream = new FileInputStream(src);
OutputStream outputStream = new FileOutputStream(dest)) {
while (true) {
byte[] buf = new byte[1024];
int n = inputStream.read(buf);
System.out.println("读到的字节个数为:" + n);
if (n == -1) {
System.out.println("读取到eof,循环结束!!");
break;
}
outputStream.write(buf, 0, n);
}
}
}
}