一、文件
计算机中,文件保存在硬盘上,但是在程序中直接操作硬盘很复杂,操作系统就将硬盘封装成 “文件”,编程时,我们只需要操作打开文件、关闭文件、读文件、写文件等接口。操作系统通过文件管理器管理文件,所有文件构成一个 N 叉树结构:
所以可以通过文件路径描述一个文件的位置。对于每一级目录,windows 系统用 /(斜杠)、\(反斜杠)来划分;Linux、MacOS、Android 等用 / 划分。有相对路径和绝对路径,相对路径是相对于基准路径。./ 表示当前目录,../ 表示上一级目录。
二、文件的种类
- 文本文件:存合法字符,数据的解析方式基于字符编码。
ascii:用数字表示英文字母、标点符号、阿拉伯数字。
后面引入更大的码表,表示汉字、俄语字符等。
gbk:2 个字符表示一个汉字。(windows 简体中文默认 gbk、VS 默认编码跟随系统)
utf-8(主流):utf-8 变长编码,2~4 个字节,一般汉字 3 个字节。
java 中一个 char 用到 utf-16(Unicode,定长) 是 2 个字符表示一个汉字,String 会在内部转为 utf-8 的形式。
- 二进制文件:存什么都可以,比如图片、视频等。
三、文件操作
- 文件系统操作:创建文件、删除文件、移动文件、获取文件属性等。
- 文件内容操作:读文件、写文件。
1、File 类(文件系统操作)
- 属性
pathSeparator :依赖系统的路径分隔符。
- 构造函数
File(String pathname):pathname 是文件路径,相对或绝对。
File(String parent, String child):与基准路径拼接,字符串形式。
File(File parent,String child):与基准路径拼接,File 类形式。
对于开发工具来说,基准路径就是终端上显示的路径:
- 方法
getParent():获取 File 的父目录。
getName():获取 File 的文件名称。
getPath():获取 File 的路径,看创建对象时的路径。
getAbsolutePath():获取 File 的绝对路径,基准路径与相对路径单纯的拼接。
getCanonicalPath():获取 File 的绝对路径,会去掉冗余的符号。
绝对路径示例:
相对路径示例:
- exists():File 文件是否真实存在。
- isDirectory():判断 File 是否是一个目录。
- isFile():判断 File 是否是一个普通文件。
- createNewFile():创建文件,创建成功返回 true。
- delete():删除成功返回 true。
- deleteOnExit():程序结束后,才会删除。(就像编辑 word,会对修改内容保存一个隐藏文件,当 word 内存中内容保存到磁盘后,程序退出,隐藏文件才会删除)
- list():列出目录下的所有文件名,返回的是 String[]。
- listFiles():列出目录下的所有文件,返回的是 File[]。
- mkdir():创建一层目录。
- mkdirs():创建多层目录。
- renameTo(File dest):文件改名或移动。
2、流对象(文件内容操作)
读(输入)文件和写(输出)文件都是相对于 CPU 的,读文件:将数据从硬盘读到内存;写文件:将数据从内存写到硬盘。
流对象主要分为两大类:
- 字节流:以字节为单位读(InputStream)写(OutputStream),常用于读写二进制文件。
- 字符流:以字符为单位读(Reader)写(Writer)(具体几个字节看编码方式,至少一个字节),常用于读写文本文件。
PS:C/C++ 没有字符流的概念,只能按字节读取。如果想读取像汉字这样的多字节字符,那么首先得识别这个字符串的二进制序列使用的什么编码方式,然后判断从哪到哪是一个完整的汉字,最后在编码表中对应起来。
2.1、InputStream(字节流)
InputStream 是个抽象类,不能创建实例,我们通过文件输入流 FileInputStream 创建实例(不仅文件有流得概念,网络等也有)。FileInputStream 会抛出 FileNotFoundException 异常。
三种参数的 read 方法:会抛出 IOException 异常,而 IOException 是 FileNotFoundException 的父类。
- read():每次读取一个字节。读取成功返回 0~255 的整数;读取失败返回 -1。
- read(byte[] b):最多读取 b.length 个字节到 b(输出型参数) 中,返回读取到的字节数,读取完毕则返回 -1(之所以设计 b 为输出型参数,是因为 java 只能有一个返回值,而已经有了返回值 n)。
- read(byte[] b, int off, int len):从 off 位置开始,最多读取 len 个字节。
- close():读取完毕后 ,需要关闭文件。进程的 PCB 中有一个文件描述符表(顺序表)的属性,它不能扩容。当程序一直运行,并打开文件却不关闭文件,那么表的空间就会耗尽。耗尽后再尝试打开,就会失败。这属于“文件资源泄露”。
但是这样写还存在一个问题,比如下面这种写法,与上面的效果一样:
将 close 写到 finally,无论以何种形式退出,都会执行 close:
但是这样写代码就很丑,可读性降低,所以我们使用 try-with-resource 写法,会自动 close:
2.2、OutputStream(字节流)
- write(int b):写入一个字节。
- write(byte[] b):写入一个字节数组。
- write(byte[] b, int off, int len):写入一个字节数组的一部分。
按 OutputStream 每次打开文件,都会清空文件内容。为了不清空,需要加上 append 参数为 true。
2.3、Reader(字符流)
read 返回的是 0x00 ~ 0xff 的一个两字节整数,需要转换为 char。
2.4、Writer(字符流)
3、案例练习
3.1、案例1
扫描指定目录,找到所有包含指定字符的普通文件,并询问用户是否删除。
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
public class Demo10 {
public static void main(String[] args) {
// 1. 接收用户输入,指定目录文件名、字符串
Scanner scanner = new Scanner(System.in);
System.out.println("输入扫描的文件名:");
String dirname = scanner.nextLine();
System.out.println("输入删除的文件名包含的字符串:");
String filestr = scanner.nextLine();
// 2. 扫描目录文件
File dir = new File(dirname);
if(!dir.isDirectory()) {
System.out.println("该目录不存在!");
}
scanDir(dir, filestr);
scanner.close();
}
private static void scanDir(File dir, String filestr) {
// 获得目录下的文件列表
File[] files = dir.listFiles();
// 判断文件列表是否为空
if(files == null) {
System.out.println("目录文件为空!");
return;
}
// 遍历文件列表,删除包含字符串的普通文件
for(File file : files) {
String filename = file.getName();
// 是一个普通文件,且文件名包含指定字符串,尝试删除文件
if(file.isFile() && filename.contains(filestr)) {
tryDeleteFile(file);
// 是一个目录文件,递归扫描
}else if(file.isDirectory()) {
scanDir(file, filestr);
}
}
}
private static void tryDeleteFile(File file) {
Scanner scanner = new Scanner(System.in);
try {
System.out.println("是否删除文件 " + file.getCanonicalPath() + "(Y or N):");
} catch (IOException e) {
e.printStackTrace();
}
String choice = scanner.nextLine();
if(choice.equalsIgnoreCase("Y")) {
if(file.delete()) {
System.out.println("文件删除成功!");
} else {
System.out.println("文件删除失败!");
}
}
scanner.close();
}
}
3.2、案例2
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;
public class Demo11 {
public static void main(String[] args) {
// 1. 接收用户输入,指定源文件、目标文件
Scanner scanner = new Scanner(System.in);
System.out.println("输入源文件路径:");
String srcname = scanner.nextLine();
System.out.println("输入目标文件路径:");
String destname = scanner.nextLine();
scanner.close();
// 2. 判断源文件是否存在,目标文件的父目录是否存在
File srcfile = new File(srcname);
File destfile = new File(destname);
if(!srcfile.isFile()) {
System.out.println("源文件不存在!");
return;
}
System.out.println(destfile.getParentFile());
if(!destfile.getParentFile().isDirectory()) {
System.out.println("目标文件的父目录不存在!");
return;
}
// 3. 读取源文件内容,并写入目标文件
copy(srcfile, destfile);
}
private static void copy(File srcfile, File destfile) {
try (InputStream inputStream = new FileInputStream(srcfile);
OutputStream outputStream = new FileOutputStream(destfile)) {
byte[] bytes = new byte[1024];
while(true) {
int n = inputStream.read(bytes);
if(n == -1) {
break;
}
outputStream.write(bytes, 0, n);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}