文件操作 – IO
文件 :
文件相比大家都不陌生把 , 打开我们的电脑,随便进入到一个盘内,都能看到一些文件, 比如 : txt 文本, jpg 图片, mp4视频 ,还有文件夹(专业一点称为目录) 这些都是文件 .
但是 文件(File) 这个概念 在计算机里 , 也是 “一词多用” 。
正因如此 我们将上面这些文件都认为是狭义的文件 ,特点就是存储在硬盘上。
而广义的文件 : 泛指计算机中很多的 软硬件资源 , 操作系统中,把很多的硬件设备和软件资源抽象成了 文件 ,按照文件的方式来统一管理.
后面学习的网络编程中 就会讲到一个很重要的设备 网卡 , 网卡 是一个硬件设备,但是在操作系统中就把网卡这样的硬件给抽象成了一个文件 。
为啥这样做呢 ?
因为这样的操作 ,可以让我们进行网络编程带来很大的便利, 比如 我们想要通过网卡接收数据,直接按照读文件代码去写就可以了 , 想要进行网卡来发送数据,直接按照写文件的代码 去写即可,这里就可以通过对文件的操作来完成对网卡的操作,简化了开发。
本文只讨论狭义的文件 , 也就是硬盘上的数据.
文件路径 :
之前我们写过的代码,存储的数据,主要是靠变量, 而变量是存储在内存中,关闭程序后这些变量就会释放,啥都没有留下,而现在我们学习的文件是保存在硬盘上的,既然保存在硬盘上,那么文件都会有属于自己的路径
知道了 路径,下面来 了解一下路径的两种表示风格
1.绝对路径 : 以盘符开头的路径
2.相对路径 : 以当前所在的目录为基准 , 以 .
或者 ..
开头 (.有时候可以省略) , 找到指定的路径
相对路径图解 :
相对路径看完, 下面来了解一下 文件的类型。
文件的类型
我们的文件有 word , exe , 图片 ,视频 , 音频 , 源代码 , 动态库 等 ,这些不同的文件, 整体 可以归纳到 两类中 。
1.文本文件 (存的是文本, 字符串)
这里字符串 , 是由字符构成的, 每个字符,都是通过一个数字来表示的, 这里文本文件,里存的数据,一定是合法的字符, 都是再你指定字符编码表之内的数据
2.二进制文件 (存的是二进制数据,不一定是字符串了)
二进制文件中 数据就没有任何限制,可以存储任何你想要的数据。
那么这里就有一个问题 ,随便给你一个文件,如何区分该文件是文本还是二进制文件呢?
其实很简单,这里直接拿记事本打开 这个文件,如果乱码就说明是二进制文件,反之就是文本文件 。
注意 : 实际写代码 的时候,这两类文件的处理方式略有区别.
了解完上面这些背景知识, 下面就来了解一下 java 对于 文件的操作 .
java 中的文件操作
这里我们主要针对两方面 :
1.针对文件系统操作 , (文件的创建, 删除, 重命名 等)
2.针对文件内容操作 (文件的读和写操作)
下面继续 , 在 java 标准库中提供了一个 File类,这个 File 类就描述了一个文件/目录 , 基于这个对象就可以实现上面这两方面.
注意, 有File 对象,并不代表真实存在该文件
下面来看看 File 类 的属性和构造方法
File 类的属性
修饰符及类型 | 属性 | 说明 |
---|---|---|
static String | pathSeparator | 依赖于系统的路径分隔符(分隔符就是 / 或者 \ ) String 类型的表示 |
static char | pathSeparator | 依赖于系统的路径分隔符,char 类型的表示 |
File 类的构造方法
签名 | 说明 |
---|---|
File(File parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径 |
File(String parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
上面这些指定的路径参数,可以是绝对路径也可以是相对路径.
parent 表示当前文件所在的目录
child 自身的文件名
在 new File 对象的时候, 在构造方法的参数中,可以指定一个路径,此时File对象就代表 这个路径对应的文件了。
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() | 判断用户是否对文件有可写权限 |
别被这些方法吓到了, 不用背,用到的时候,查一下即可,下面就来使用几个方法 。
File 类的使用
图一 :
图二 : 相对路径
下面继续 :
这里来看三个比较实用的
1.判断文件是否真实存在 : exists()
2.判断文件是否是一个普通文件 : isFile()
3.判断文件是否是一个目录 : isDirectory()
可以看到我们再绝对路径下,使用这几个方法结果都是false , 表示在d盘下没有test.txt 这个文件,下面就来学习一下如何创建文件
这里需要使用 File 类 提供的方法 : createNewFile
既然有创建文件的方法,那么肯定有删除文件的方法 ,下面就来使用一下 delete 方法
关于删除文件的方法还有一个方法 : deleteOnExit ()
使用 deleteOnExit () 方法, 会在程序退出的时候,自动删除.
使用场景 : 程序中需要使用到一些 “临时文件” 的时候 ,就需要用到。
举个例子 :
了解完删除方法, 下面我们来创建一个目录 。
创建目录 : mkdir()
注意 : 这里的 mkdir方法只能创建 一级目录,如果想要创建多级目录 需要使用 mkdirs 方法
创建多级目录方法 : mkdirs ()
创建目录看完,下面使用 ,重命名方法 : renameTo ()
演示 :
重命名看完,下面来看看 list 、listFiles
这两个方法
list () : 返回 File 对象代表的目录下的所有文件名
返回 File 对象代表的目录下的所有文件,以 File 对象表示
演示 :
图二 :
到此 文件操作的相关方法差不多就看完了, 下面来学习一下, 关于 文件内容相关的操作
文件内容的相关操作
这里针对文件内容 ,使用 “流对象” 进行操作 .
解释一下 啥叫流 :
这里的流对象 是形象的比喻 , 在计算机里的很多概念,都使用了一定的修辞手法 .
这里的比喻就是一种常见的方法 .
另外 : 使用一个链表头节点/二叉树根节点,表示整个链表/二叉树 , 这也是一个修辞手法 。
此时使用的就是借代(局部表示整体),借代也是一种常见的修辞手法
下面继续
谈到流 这里第一印象 就是 水流 特点:生生不息 ,绵延不断 , 这里的流对象就是这样的 。
这里就可以想象一个 场景 :有一个水龙头 ,想要通过这个水龙头 可以接水 ,
假设 需要接100ml 的水 ,
这里就可以采用 一次接 100ml , 一次接完 , 也可以 一次接 50ml 分两次接 ,还可以一次接 10ml 分 10次 … 。
关于这里的装水,我们就可以随心所欲的安排 一次装多少水 ,只需要保证最终接收到 100ml 的水
这就是 流的一个特点 : 可以随心所欲的调整接收的数据量
对比一下 其他固体 的东西, 比如 体育室 里面的 篮球,这里就只能一个一个的拿, 能 一次性拿1.5 个吗显然是不可以的 ,这里就不是流 。
我们想要从文件读一百个字节
1.可以 一次 读 100个字节, 分一次读完
2.可以 一次 读 50个字节,分两次 读完
3.可以 一次 读 10个字节,分十次 读
…
这里我们读文件操作是不是就和 水流接水的操作相似 , 写文件同理 ,因此就把读写文件的相关操作称为 “流对象”
附上一张图:
知道了啥是流 , 下面来看看 java 标准库的流对象 .
这里从类型上,分为两个大类
1.字节流
字节流 : 针对二进制文件 ,以字节为单位进行读写
读相关的类
InputStream 抽象类
, FileInputStream 对应子类
写相关的类
OutputStream 抽象类
, FileOutPutStream 对应的子类
2.字符流
字符流 : 针对文本文件 , 以字符为单位进行读写
读相关的类
Reader 抽象类
, FileReader 对应的子类
写相关的类
Writer 抽象类
, FileWriter 对应的子类
别看这里的类很多,其实这里的类使用方式是非常固定的
核心就四个操作
1.打开文件. (构造对象)
2.关闭文件. (close)
3.读文件 . (read) --> 针对 InputStream / Reader
4.写文件 .(write) --> 针对 OutputStream / Write
说了这么多,下面来写个代码演示一下如何使用 .
字节流的读和写操作
读文件 : InputStream
图一 :
图二 : 带有一个参数版本的 read
这里输出型参数 ,就好比食堂打饭,将餐盘给 食堂阿姨,阿姨给你打饭 ,打完饭再将 餐盘还给你 .
这里 read 带有三个参数版本的 与 带有一个版本的类似 简单演示一下
buffer 名词解释 :
这里可以想象成 嗑瓜子 , 没有使用 缓冲区(buffer) , 磕一个瓜子 , 抛一次壳 (进行一次 IO 操作) , 这样嗑瓜子明显就非常麻烦 , 将我们的 手看成一个缓冲区, 当我们磕满一个手的瓜子壳 (此时相当于进行了一次IO操作, 读取了一个数组长度的数据 ),再去抛壳,是不是 效率就高了。。
读文件看完 , 下面瞧瞧写文件
写文件 :OutputStream
图一 :
有没有好奇,为啥 input 是读操作 , 而 output 是写操作 , input 翻译过来是 输入的意思 output 翻译过来是 输出的意思,那么为啥 输入不是写操作而是读操作呢 ?
关于这个问题 : 这里就需要注意 input 和 output 的方向,z这里的方向是以 CPU 为中心 进行看待的 ,
下面来看一个比较重要的东西 .
图一 :
图二 :
到此 字节流 的 读和写操作 就看完了, 下面来 简单 演示一下 字符流的读和写操作 (因为这些类的用法都一样)
字符流的读和写操作
1.字符流 读操作
2.写操作
补充 :flsush 方法 手动刷新缓冲区
上面 解释名词 buffer ,了解了一下缓冲区 ,并且知道了缓冲区存在的意义就是为了提高效率 .
下面就针对缓冲区 补充一个方法 flush , 这个方法是用来刷新缓冲区的 。
比如 : 在写数据的时候,需要把数据写到缓冲区里,然后再统一写入到硬盘, 如果当前的缓冲区已经写满了,就直接触发写硬盘操作,如果当前的缓冲区还没写满,如果 想提前将数据写入到硬盘里,这时就可以通过 flush 来手动 刷新缓冲区 .
之前 的代码 ,没有涉及到 flush , 是因为 代码并没有涉及到很多数据,很快执行完了 try 语句,而 try 语句结束的时候,会自动调用 close 方法 进行释放文件对象,关闭文件, 此时close 就会刷新缓冲区 。
再来补充一点 :Scanner 可以搭配流对象进行使用
此时 使用 scanner 读取 一个文件,就比较方便了 ,
最后再来补充一个 :使用 PrintWriter 类 进行写操作
PrintWriter 类 提供了 我熟悉的 print , printf , println 方法,方便我们进行写操作
此时关于文件内容就看完了,主要就是那几个类 , 下面就写几个关于文件的代码案例来熟悉一下 这些 API 。
代码案例
代码案例一 :
案例1:扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要删除该文件
这里会先 给顶一个目录, 目录里会包含很多的文件和子目录…
用户输入一个要查询的词 ,看看当前目录下(以及子目录里) 是否有匹配的结果 (文件名匹配) , 如果有匹配的结果,就进行删除
import java.io.File;
import java.util.Scanner;
public class Test4 {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
// 让用户输入一个指定搜索的目录
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的路径: ");
String basePath = scanner.next();
// 针对用户输入进行简单判定
File root = new File(basePath);
// isDirectory() 判断当前的文件是否为目录文件
if (!root.isDirectory()) {
// 路径 不纯在, 或者知识一个普通的文件, 此时无法进行搜索
System.out.println("输入的目录有误!");
return;
}
// 前面没问题 此时再让用户输入一个 要删除的文件名
System.out.println("请输入要删除的文件名: ");
// 此处要使用 next , 而不要使用 nextLine !!!
String nameToDelete = scanner.next();
// 针对指定的路径进行扫描 , 这里就可以采用递归操作
// 先从我们的 根目录触发 (root)
// 先判定一下 , 当前的这个目录 ,是否包含要删除的文件,如果是就删除 ,否则就跳过下一个
// 如果当前这里包含了一些目录 ,再针对子目录进行递归.
scanDir(root, nameToDelete);
}
private static void scanDir(File root, String nameToDelete) {
// 1. 先列除当前路径下包含的内容
File[] files = root.listFiles();
if (files == null) {
// 当前 root 目录下没东西 , 是一个空目录
// 此时就结束继续递归
return;
}
// 2. 遍历当前的列出结果
for (File f : files) {
if (f.isDirectory()) {
// 如果是目录就进一步 递归
scanDir(f, nameToDelete);
} else {
// 如果是普通文件,则判定是否要删除
if (f.getName().contains(nameToDelete)) {
// 这里可以再判断一下
System.out.println("确认是否要删除 " + f.getAbsolutePath() + " 吗?");
String choice = scanner.next();
if (choice.equals("y") || choice.equals("Y")) {
f.delete();
System.out.println("删除成功!");
} else {
System.out.println("删除取消!");
}
}
}
}
}
}
代码案例二 :
案例二 : 普通文件的复制
要求 : 把一个文件拷贝成另一个文件.
这里就非常简单, 就是把第一个文件按照字节依次读取,把结果写到另一个文件中 .
import java.io.*;
import java.util.Scanner;
public class Test5 {
public static void main(String[] args) {
// 输入两个路径.
// 源 和 目标 . (从那里拷贝到哪里)
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要拷贝那个文件: ");
String srcPath = scanner.next();
System.out.println("请输入要被拷贝到那个地方: ");
String destPath = scanner.next();
File srcFile = new File(srcPath);
if (!srcFile.isFile()) {
// 如果 源 不是一个文件(不存在或者是目录) ,此时不做任何操作
System.out.println("您当前输入的源路径有误");
}
File destFile = new File(destPath);
if (destFile.isFile()) {
// 如果已近存在 认为也不能拷贝 (比如说拷贝后的名字叫 坤.txt , 但是 目标目录下已经存在 坤.txt 此时拷贝相当于覆盖掉原来的文件)
System.out.println("您当前输入的目标路径有误!");
return;
}
//
// if (destFile.exists()) {
//
// if (destFile.isDirectory()) {
// System.out.println("目标路径已存在 ,并且是一个目录,请确认路径是否正确");
// return;
// }
//
// if (destFile.isFile()) {
// System.out.println("目录路径已经存在,是否要进行覆盖? 覆盖 输入 y");
// String ret = scanner.next();
//
// if (!ret.toLowerCase().equals("y")) {
// System.out.println("停止覆盖");
// return;
// }
//
// }
// }
// try(InputStream inputStream = new FileInputStream(srcFile)){
//
// try(OutputStream outputStream = new FileOutputStream(destFile)){
//
// }
//
// }catch (IOException e){
// e.printStackTrace();
// }
// 进行拷贝操作
try (InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputStream = new FileOutputStream(destFile)) {
// 进行读文件操作.
while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);
if (len == -1) {
break;
}
outputStream.write(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码案例三 :
案例三 : 扫描指定目录 , 并找到名称或者内容中包含指定字符的所有普通文件 (不包含目录)
这里就是进行文件内容的查找
主要分为三步骤
1.输入一个路径
2.在输入一个要查找文件的关键字
3.递归的遍历文件 , 找到含有关键字的文件,将对应的文件路径打印出来。
其实 这个代码案例 就与第一个代码案例非常相似 .
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Scanner;
public class Test6 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("输入要扫描的路径: ");
String rootDirectoryPath = sc.next();
// 换一行
System.out.println();
System.out.print("输入要查询的文件名关键字: ");
String keyword = sc.next();
System.out.println();
File file = new File(rootDirectoryPath);
if (!file.isDirectory()) {
// 此时不是目录文件, 表示当前的路径有误
System.out.println("输入的路径错误! ");
return;
}
// 进行递归遍历
scannerDirectory(file, keyword);
}
private static void scannerDirectory(File file, String keyword) {
// 1. 先列出 file 中 有那些内容
File[] files = file.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isFile()) {
// 此时 f 是普通的文件 ,
// 针对普通文件的内容 进行判断 , 是否满足查询条件 (文件内容是否包含 关键字 keyword)
if (containsKeyword(f, keyword)) {
// 此时普通文件中 含有我们需要查询的关键字, 将路径打印出来
System.out.println(f.getAbsolutePath());
}
} else if (f.isDirectory()) {
// 此时 f 为目录文件,那么进入 目录 文件, 然后继续递归查找
scannerDirectory(f, keyword);
}
}
}
private static boolean containsKeyword(File f, String keyword) {
// 将 f 中的内容 读出来 ,进行遍历 ,放到一个 StringBuilder 中
StringBuilder stringBuilder = new StringBuilder();
try (Reader reader = new FileReader(f)) {
char[] buffer = new char[1024];
while (true) {
int len = reader.read(buffer);
if (len == -1) {
break;
}
// 将读取到的结果 放到 StringBuilder 中
stringBuilder.append(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
// indexOf 返回的是子串 (keyword) 的下标 , 如果 word 在StringBuilder 中不存在 ,则返回 -1
return stringBuilder.indexOf(keyword) != -1;
}
}
图示 :
小补充一点 : OutputStream 在写文件的时候,文件不存在 就会自动创建, InputStream 不行, 文件不存在,就抛异常了
到此 文件操作 就结束了, 比较简单, 就是一些 API 的使用 。