一、认识文件
我们先来认识狭义上的文件(file)。针对硬盘这种持久化存储的I/O设备,当我们想要进行数据保存时,往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份份真实的文件一般。
文件除了有数据内容之外,还有一部分信息,例如文件名、文件类型、文件大小等并不作为文件的数据
而存在,我们把这部分信息可以视为文件的元信息
平时说的文件一般都是指存储在硬盘上的普通文件。
形如:txt文本、jpg图片、mp4视频、rar压缩包等这些文件都可以认为是普通文件。
它们都是在硬盘上存储的。
但是站在计算机专业术语的角度上来讲:
在计算机中文件可能是一个广义的概念,就不只是包含普通文件,还可以包含目录(把目录称为目录文件),目录俗称文件夹。
在操作系统中,还会使用文件来描述一些其它的硬件设备或者软件资源。
其实站在操作系统的角度上来看,所谓的文件,其实是一个非常广义的概念。
不仅仅指的是我们日常生活中所说的这些普通文件,也包含目录,还包含一些软硬件资源设备。
后面讲网络编程的时候,会讲到一个很重要的设备“网卡”。
网卡,是一个硬件设备。但是在操作系统中就把 网卡 这样的硬件设备也给抽象成了一个文件。
这样的操作,会给我们进行网络编程带来很大便利。
我们要想通过网卡来收数据,直接按照 读文件代码 去写 就可以了。
想通过网卡来发数据,那就按照 写文件代码 去写就行了
这样做带来的好处:简化开发。
通过这种文件的操作,来完成操作网卡的过程。
除此之外,还有显示器,键盘,这都是硬件设备吧。
操作系统也是把这些设备视为文件。
想从键盘读取数据,其实也是通过类似读文件的方式来进行的。
想往显示器上写一个数据,也是通过类似写文件的方式来进行的。
因此,操作系统谈到的文件,其实是一个更加广泛的概念。它会涉及到各个方面的情况。
当前我们需要讨论的文件,主要还是针对普通文件来讨论的。
后面去学习一些其他的硬件设备对应的文件,其实也是通过类似的代码来实现操作。
因此,我们虽然讨论的是针对普通文件为例来去演示,但实际上这里的一些代码,和后需用到的一些代码,尤其是网络编程的代码,是非常具有共性的。
二、普通文件 与 机械硬盘
普通文件是保存在硬盘上的。【一般讨论的都是机械硬盘】
机械硬盘一旦通电,里面的盘片就会高速运转,
例如:我们使用的是 7200转/分钟 的家用机械硬盘,那么,每秒就是 120转/秒。
磁头就会在盘片上找到对应的数据。
因为盘片上是有一圈圈 磁道的。
然后呢,我们就把数据 去按照这一圈一圈的方式来往上去写。
因为这里面的盘片,其实是一个非常精密的设备。
这里面是可以存储很多数据的。
像这么小的硬盘(大概只有现在手机的一半大小),就可以存储上T的数据。
但是 存储空间的大小,受限于机械硬盘的硬件结构。
盘片转速越高,读写速度也就越快。
但是因为工艺的限制,盘片的转速也不可能无限高。
(现在最好的机械硬盘大概是每分钟 1 万 多 转)
另外,机械硬盘的读写速度,已经有10年 停滞不前了。【技术上无法精进】
现在的机械硬盘都是往大容量的方向发展。
前面的博文也说到过,内存的读写速度 比 现在的硬盘读写速度 快很多!!!
快 3-4 个数量级,大概是上万倍。
后面为了解决这个问题,就有了固态硬盘(SSD)。
固态硬盘的硬件结构 和 机械硬盘 截然不同。
固态硬盘就是一个大号的U盘。
U盘:里面可以存储数据,但是又不需要盘片这些东西。主要是 flash 芯片(闪存),通过这个东西来进行存储数据,而且能获取到更高速率的读写。
所以,固态硬盘的读写速度要比机械硬盘高很多。【高好几倍】
现在最好的固态硬盘,读写速度已经接近十几年前的内存水平了。
本篇博文主要讨论的是 以机械硬盘为主。 因为当前企业中使用的服务器还是以机械硬盘为主。
因为 SSD 的成本 是 机械硬盘的好几倍。(4-5倍)
三、文件的分类
站在程序员的角度,主要把文件分成两类:
1、文本文件:里面存储的是字符
2、二进制文件:里面存储的是字节
针对这两种文件,在编程的时候会存在差异。
3.1、如何判断是文本文件还是二进制文件
我们判定一个文件是文本 ,还是 二进制文件,有一个简单直接的方法。
就是 把一个文件直接用记事本打开,如果是乱码就是 二进制文件,如果不是乱码就是文本。
四、文件的目录结构
随着文件越来越多,对文件的系统管理也被提上了日程,如何进行文件的组织呢,一种合乎自然的想法出现了,就是按照层级结构进行组织 —— 也就是我们数据结构中学习过的树形结构。这样,一种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹(folder)或者目录(directory)的概念
也就是说普通文件,一定是树的叶子结点。
目录文件,就可以看作是一个子树,也就是非叶子节点。
五、文件路径(Path)
在操作系统中,就通过“路径” 这样的概念,来描述一个具体 文件/目录 的位置。
5.1、绝对路径
以盘符开头的。【以 C、D、E、F等等,盘名开头的】
5.2、相对路径
以 . 或者 … 开头的文件路径。
其中 . 表示当前路径, … 表示当前路径的父目录(上级路径)。
谈到相对路径,必须要有一个基准目录。
对于路径就是从基准目录出发,按照一个什么样的路径找到的对应文件。
注意!即使是定位到同一个文件,如果基准路径不同,此时的相对路径也就不同
绝对路径 和 相对路径 的概念非常重要!!!!
虽然面试不考,但是我们在后面的编程中,需要频繁的和这些路径打交道。、
六、java 中操作文件
Java中操作文件,主要是包含两类操作
1、文件系统相关的操作
2、文件内容相关的操作
6.1、文件系统相关的操作
这个是C语言中没有的操作。C语言标准库不支持这个操作。
文件系统相关的操作:指的是通过“文件资源管理器” 能够完成的一些功能
它具有的功能
1、列出目录中有哪些文件
2、创建文件(直接右键点击菜单创建,就可以创建了)
3、创建 目录/文件夹
4、删除文件
5、重命名文件
…
这些都是文件资源管理器所能够完成的功能。
简单来说 文件资源管理器 能做什么,我们的文件系统相关操作的代码就能做什么。
在Java中提供了一个 File 类,通过这个类来完成上述操作。
首先,这个File类就描述了一个文件/目录。
基于这个对象就可以实现上面的功能。
6.2、File类中的属性
修饰符及类型 | 属性 | 说明 |
---|---|---|
static String | pathSeparator | 依赖于系统的路径分隔符,String 类型的表示 |
static char | pathSeparator | 依赖于系统的路径分隔符,char 类型的表示 |
6.3、File类中的构造方法
签名 | 说明 |
---|---|
File(File parent, Stringchild) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径 |
File(String parent, Stringchild) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
File 的构造方法,能够传入一个路径,来指定一个文件。
这个路径可以是绝对路径,也可以是相对路径。
6.4、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(Filedest) | 进行文件改名,也可以视为我们平时的剪切、粘贴操作 |
boolean | canRead() | 判断用户是否对文件有可读权限 |
boolean | canWrite() | 判断用户是否对文件有可写权限 |
6.5、代码示例
6.5.1、File类的构造方法
通过绝对路径和相对路径来指向一个文件
6.5.2、File类的普通方法
目前上述操作,都是针对文件系统的操作。
主要还是 增删改查 的一些操作。
但是呢,这些操作只是作用于文件表面,并没有对文件的内容做出实质性的改变。
getParent、getName、getPath、getAbsolutePath、getCanonicalPath
public class Dome1 {
public static void main(String[] args) throws IOException {
File f1 = new File("D:test.txt");
System.out.println(f1.getPath());//返回 File 对象的文件路径
System.out.println(f1.getParent());//返回 File 对象的父目录文件路径
System.out.println(f1.getName());//返回 FIle 对象的纯文件名称
System.out.println(f1.getAbsolutePath());//返回 File 对象的绝对路径
System.out.println(f1.getCanonicalPath());//返回 File 对象的修饰过的绝对路径
System.out.println("--------------------------");
File f2 = new File("./test.txt");
System.out.println(f2.getPath());//返回 File 对象的文件路径
System.out.println(f2.getParent());//返回 File 对象的父目录文件路径
System.out.println(f2.getName());//返回 FIle 对象的纯文件名称
System.out.println(f2.getAbsolutePath());//返回 File 对象的绝对路径
System.out.println(f2.getCanonicalPath());//返回 File 对象的修饰过的绝对路径
}
}
exists、isDirectory、isFile
public class Dome2 {
public static void main(String[] args) {
File f1 = new File("d:/test.txt");
System.out.println(f1.exists());//判断 File 对象描述的文件是否真实存在
System.out.println(f1.isDirectory());//判断 File 对象代表的文件是否是一个目录
System.out.println(f1.isFile());//判断 File 对象代表的文件是否是一个普通文件
}
}
createNewFile
public static void main(String[] args) throws IOException {
File f1 = new File("d:/test.txt");
//根据 File 对象,自动创建一个空文件。成功创建后返回 true
System.out.println(f1.createNewFile());
}
delete
public static void main(String[] args) {
File f1 = new File("d:/test.txt");
System.out.println(f1.delete());//根据 File 对象,删除该文件。成功删除后返回 true
}
mkdir、mkdirs
public static void main(String[] args) {
//在当前项目底下创建一个 aaa 目录
File file = new File("./aaa");
System.out.println(file.isDirectory());
file.mkdir();// mkdir 方法 只能创建一级目录
System.out.println(file.isDirectory());
}
与mkdir方法,相似的方法还有一个 mkdirs 方法,
意思创建多级目录。
public static void main(String[] args) {
//在当前项目底下创建一个 aaa 目录
File file = new File("./aaa/bbb/ccc");
System.out.println(file.isDirectory());
file.mkdirs();
System.out.println(file.isDirectory());
}
目前上述操作,都是针对文件系统的操作。
主要还是 增删改查 的一些操作。
但是呢,这些操作只是作用于文件表面,并没有对文件的内容做出实质性的改变。
七、文件内容相关操作
1、打开文件
2、读文件
3、写文件
4、关闭文件
针对文件内容的读写,Java提供了一组类来完成上述操作
7.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 只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个 InputStream 类,我们现在只关心从文件中读取,所以使用 FileInputStream
7.1.1、FileInputStream 概述
构造方法
签名 | 说明 |
---|---|
FileInputStream(File file) | 利用 File 构造文件输入流 |
FileInputStream(String name) | 利用文件路径构造文件输入流 |
7.1.2、针对字节的读操作
FileInputStream 一个字节一个字节的读
FileInputStream 一次读取若干个字节
利用 Scanner 进行字符读取
上述例子中,我们看到了对字符类型直接使用 InputStream 进行读取是非常麻烦且困难的,所以,我
们使用一种我们之前比较熟悉的类来完成该工作,就是 Scanner 类
构造方法 | 说明 |
---|---|
Scanner(InputStream is, String charset) | 使用 charset 字符集进行 is 的扫描读取 |
// 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容
public class Main {
public static void main(String[] args) throws IOException {
try (InputStream is = new FileInputStream("hello.txt")) {
try (Scanner scanner = new Scanner(is, "UTF-8")) {
while (scanner.hasNext()) {
String s = scanner.next();
System.out.print(s);
}
}
}
}
}
7.2、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
7.2.1、针对字节的写操作
FileOutputStream 一次写一个字节
FileOutputStream 一次写若干字节
利用 PrintWriter 找到我们熟悉的方法
上述,我们其实已经完成输出工作,但总是有所不方便,我们接来下将 OutputStream 处理下,使用
PrintWriter 类来完成输出,因为
PrintWriter 类中提供了我们熟悉的 print/println/printf 方法
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();
}
}
}
}
7.3、FileReader 针对字符的读操作
7.4、FileWriter 针对字符的写操作
八、小程序练习
8.1、扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
意思就是:
用户输入一个目录
再输入一个要删除的文件名
找到名称中包含指定字符的所有普通文件
询问用户是否要删除该文件
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Scanner;
/**
* 扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要
* 删除该文件
*/
public class Dome1 {
public static void main(String[] args) {
//1、首先输入扫描的目录,以及要删除的文件
Scanner scanner = new Scanner(System.in);
System.out.println("请输入扫描的路径");
String rootDirPath = scanner.next();
System.out.println("请输入要删除的文件名");
String toDelFileName = scanner.next();
File rootDir = new File(rootDirPath);
//2、判断一下要删除的文目录是否存在
if(!rootDir.isDirectory()){
return;
}
//3、搜到这里说明当前目录是存在的
//4、写一个函数,实现删除文件
scanDir(rootDir,toDelFileName);
}
//通过这个方法实现删除文件
private static void scanDir(File rootDir, String toDelFileName) {
//1、首先列出 rootDirPath 有哪些内容
File[] files = rootDir.listFiles();
if(files == null){
// rootDir是一个空目录
return;
}
//走到这里,说明 rootDir 一个空目录,我们就要进要遍历目录,然后删除文件
for (File f : files) {
if(f.isFile()){
//普通文件的情况
if(f.getName().contains(toDelFileName)) {
//不要求文件名字一样,只要求包含了关键字的文件都要删除
deleteFile(f);
}
}else if(f.isDirectory()){
scanDir(f,toDelFileName);
}
}
}
private static void deleteFile(File f) {
try {
System.out.println("确认要删除文件吗(Y/N) + " + f.getCanonicalPath());
Scanner scanner = new Scanner(System.in);
String chioce = scanner.next();
if(chioce.equals("Y") || chioce.equals("y")){
f.delete();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
8.2、进行普通文件的复制
文件复制,其实说白了:就是将文件给拷贝一个副本。
我们需要让用户指定两个文件路径。
一个是原路径(被复制的文件)
一个是目标路径(复制之后生成的文件)
从哪里复制到哪里,从哪里来到哪来去。
我们需要描述清楚 起点 和 终点。
然后,我们就可以在这个基础上进行具体复制操作
我们要做的事很简单。
打开源路径文件,读取里面的内容,并写入到目标文件中,
/**
* 进行普通文件的复制
*/
public class Dome2 {
public static void main(String[] args) {
//1、首先输入两个路径
Scanner scanner = new Scanner(System.in);
System.out.println("请输入源路径:");
String src = scanner.next();
System.out.println("请输入目标路径:");
String dest = scanner.next();
File scrFile = new File(src);
//2、判断一下输入的源路径是否存在
if(!scrFile.isFile()){
System.out.println("输入的源路径错误!");
return;
}
//此处不需要检查目标路径是否存在, OutputStream 进行写操作的时候,如果文件不存在会自动创建文件
//3、将源文件拷贝到目标路径里面
try (InputStream inputStream = new FileInputStream(scrFile)){
try (OutputStream outputStream = new FileOutputStream(dest)){
//把inputStream 中的数据读出来,写入到outputStream里面
byte[] buffer = new byte[1024];
while (true){
int len = inputStream.read(buffer);
if(len == -1){
//读取完毕
break;
}
//进行写入数据
//在写入数据的时候,不能将整个buffer进行写入,毕竟 buffer 可能只有一部分数据是有效数据
outputStream.write(buffer,0,len);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
8.3、扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
进行文件内容的查找
1、输入一个路径
2、再输入一个要查找文件的 关键字
3、递归的遍历文件,找到看哪个文件里的内容包含了那些关键词,就把对应的文件路径打印出来。
思路和案例1一样,先递归遍历文件,针对每个文件都打开,并读取内容,再进行字符串查找即可。
public class Dome3 {
public static void main(String[] args) throws IOException {
//1、首先输入要扫描的路径
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要扫描的路径:");
String rootDirPath = scanner.next();
System.out.println("请输入要查找的关键词:");
String word = scanner.next();
File rootDir = new File(rootDirPath);
if(!rootDir.isDirectory()){
System.out.println("没有输入的路径!");
return;
}
//实现一个函数,这个函数就用来删除办好关键词的文件
scanDir(rootDir,word);
}
//递归遍历这个路径的所有文件内容
private static void scanDir(File rootDir, String word) throws IOException {
//1、首先列举出所有目录
File[] files = rootDir.listFiles();
if(files == null){
//说明没有这个目录
return;
}
//2、遍历每个元素,针对目录或者文件进行操作
for (File f : files) {
if(f.isFile()){
//针对文件内容进行查找
if(contansWord(f,word)){
System.out.println(f.getCanonicalPath());
}
}else if(f.isDirectory()){
//针对目录进行递归遍历
scanDir(f,word);
}
}
}
private static boolean contansWord(File f, String word) {
//将文件的内容读出来,放到一个StringBuffer里面
StringBuffer stringBuffer = new StringBuffer();
try (Reader reader = new FileReader(f)){
char[] buffer = new char[1024];
while (true){
int len = reader.read(buffer);
if(len == -1){
break;
}
stringBuffer.append(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}
//indexOf 返回得是子串的下标
return stringBuffer.indexOf(word) != -1;
}
}