文件操作和IO
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
专栏简介: JavaEE从入门到进阶
题目来源: leetcode,牛客,剑指offer.
创作目标: 记录学习JavaEE学习历程
希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.
学历代表过去,能力代表现在,学习能力代表未来!
目录
1.认识文件
1.1 计算机中的文件有狭义与广义之说:
- 狭义上的文件指的是 , 针对硬盘这种持久化存储的I/O设备 , 保存数据时会分割成一个个独立的单位 , 这些独立的单位就被抽象成文件的概念.
- 广义上的文件指的是 , 操作系统中会把很多的硬件设备和软件资源抽象成文件 , 按照文件的方式来统一管理 , 例如: 网卡这个硬件设备在网络编程中 , 通常会被当做文件来操作.
文件中除了有数据内容之外 , 还有一部分信息如: 文件名 文件类型 文件大小等不作为文件的数据而存在 , 我们把这些信息称为描述文件的元信息.
1.2 树形结构组织和目录
随着文件数量的越来越多 , 如何高效的管理文件提上日程 , 为了将文件按照层级结构进行组织 , 树形存储结构应用而生 , 这样 , 一种专门用来存放管理信息的文件诞生了 , 也就是我们常说的文件夹(folder)和目录(directory).
文件夹和目录中保存的就是我们之前提到的元信息.
1.3 文件路径(Path)
如何在文件中定位我们要找的唯一文件 , 就是当前要解决的问题 , 从树形角度的结构来看 , 树的每个节点都是从一条根开始一直到达节点的路径 , 这种描述方式被称为文件的绝对路径(absolute path).
1).绝对路径风格:
以 c: d: 盘符开头的路径
2).相对路径:
以当前所在的工作目录为基准 , 找到指定的路径.
如果工作目录不同 , 定位到同一个文件 , 相对路径的写法不同.
例如: 定位到 d:/tmp/111
如果工作目录是 d:/ 相对路径写作 ./tmp/111
如果工作目录是 d:/tmp 相对路径写作 ./111
如果工作目录是 d:/tmp/222/111 相对路径写作 ../111(..表示当前目录的上级目录)
如果工作目录是 d:/tmp/222/bbb/111 相对路径写作 ../../111
1.4 文本文件和二进制文件
Windows 系统中对文件类型的区分非常详细 , word,exe,图片,视频,音频,源代码,动态库....这些不同的文件整体可用归为 文本文件 和 二进制文件.
- 文本文件: 存的是字符串 , 都是由字符构成 , 每个字符都是通过一个数字来表示 , 这些字符一定是合法的 , 都是在我们指定字符编码的码表之内的数据.
- 二进制文件: 没有任何限制 , 可以存储你想要的任何数据.
如果判断文本文件和二进制文件?
有一个简单粗暴地方式就是用记事本打开 , 因为记事本是文本文件 , 如果打开以后乱码了就是二进制文件 , 如果没乱码就是文本文件.
2.Java中操作文件--File类
Java 对文件的操作可以分为:
- 1. 针对文件系统操作.(文件的创建 , 删除 , 重命名)
- 2. 针对文件内容操作.(文件的读和写)
Java 标准库中 , 提供了一个 File类 , 专门用来进行文件系统操作 , 注意! 有File对象并不代表有实际的文件/目录.
2.1 属性
pathSeparator 是 File 中的一个静态变量 , 表示 / 或 \ 根据具体的系统表示.
修饰符及类型 | 属性 | 说明 |
static String | pathSeparator | 依赖于系统的路径分隔符 , String 类型的表示 |
static char | pathSeparator | 依赖于系统的路径分隔符 , char 类型的表示 |
2.2 构造方法
签名 | 说明 |
File(File parent , String child) | 根据父目录+还在文件路径 , 创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例 , 路径可以是绝对路径 或相对路径 |
第二种是最常见的构造方法.
2.3 方法
File 方法较为简单 , 通过方法名即可知道方法的作用.
修饰符及返回类型 | 方法签名 | 说明 |
String | getParent() | 返回 File 对象的父目录文件路径 |
String | getName() | 返回 FIle 对象的纯文件名称 |
String | getPath() | 返回 File 对象的文件路径(可能是绝对也可能是相对) |
String | getAbsolutePath() | 返回 File 对象的绝对路径 |
String | getCanonicalPath() | 返回 File 对象修饰的绝对路径 |
boolean | isDirectory() | 判断 File 对象代码的文件是否是一个目录 |
boolean | exists() | 判断 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() | 判断用户是否对文件有可写权限 |
示例一:测试 get系列的特点和差异
public static void main(String[] args) throws IOException {
File file = new File("d:/text.txt");
System.out.println(file.getName());
System.out.println(file.getParent());
System.out.println(file.getPath());
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalFile());
}
示例二:普通文件的判断
public static void main(String[] args) throws IOException {
File file = new File("d/text.txt");
System.out.println(file.exists());
System.out.println(file.isFile());
System.out.println(file.isDirectory());
}
示例三:观察 deleteOnExit 的现象
public static void main(String[] args) throws IOException {
File file = new File("some-file.txt");//要求该文件不存在才能看到相同的现象
System.out.println(file.exists());
System.out.println(file.createNewFile());
System.out.println(file.exists());
file.deleteOnExit();
System.out.println(file.exists());
}
当程序运行结束后文件才会被删除 , 类似于平常编写word文档时 , 系统会自动打开一个临时版本的word文档保存临时数据 , 当word文档被突然关闭 , 临时word文档也会随之关闭 , 当下次打开时会询问你是否要恢复临时文件中编辑的内容.
示例四: 观察目录的创建
public static void main(String[] args) throws IOException {
File file = new File("some-dir");//要求该文件不存在才能看到相同的现象
System.out.println(file.isDirectory());
System.out.println(file.isFile());
System.out.println(file.mkdir());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
}
3.文件内容的读写--数据流
针对文件内容 , 使用"流对象" 进行操作 , 将文件内容中的数据类比于水流. 这是因为读/写文件和接/灌水类似 , 水可以任意ml的接/灌 , 数据也可以任意byte的读/写.
Java 标准库的流对象 , 从类型上分成两个大类:
- 1.字节流: InputStream OutputStream 以字符为单位读取.
- 2.字符流: Reader Writer 以字符为单位读取.
这些类的使用非常固定 , 核心就是四个操作.
- 1.打开文件.(构造对象)
- 2.关闭文件.(close)
- 3.读文件.(read)=>针对InputStream/Reader
- 4.写文件.(writer)=>针对OutputStream/Writer
Tips: InputStream/OutputStream/Reader/Writer 都是抽象类 , 不能直接 new 还需要具体的实现类. 关于具体的实现类有很多 , 我们现在只关心从文件中读写 , 所以使用 FileInputStream/FileOutputStream/FileReader/FileWriter.
3.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 | 关闭字节流 |
3.2 FileIntputStream 概述
构造方法:
签名 | 说明 |
FileInputStream(File file) | 利用File构造文件输入流 |
FileInputStream(String name) | 利用文件路径构造输入流 |
示例一:read 读取文件第一版
读取完毕后返回 -1.
public static void main(String[] args) throws IOException {
//创建InputStream对象时,使用绝对路径和相对路径都可以,也可以使用File对象
InputStream inputStream = new FileInputStream("./text.txt");
//进行读操作
while (true){
int b = inputStream.read();
if(b == -1){
//读完完毕
break;
}
System.out.println((byte)b);
}
inputStream.close();
}
文件中内容为 "hello"
读取结果:
示例二: read 读取文件第二版
read 读取文件的第二版需要提前准备好一个字节数组 , 然后将字节数组作为参数传给 read 方法 , 让 read 内部对这个数组进行填写.(此处参数相当于输出形参数)
public static void main(String[] args) throws IOException {
//创建InputStream对象时,使用绝对路径和相对路径都可以,也可以使用File对象
InputStream inputStream = new FileInputStream("./text.txt");
while (true){
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);
System.out.println("len:" +len);
if(len == -1){
break;
}
//此时读到的结果就放到了 buffer中
for (int i = 0; i < len; i++) {
System.out.printf("%x\n",(byte)buffer[i]);
}
}
inputStream.close();
}
文案内容为 "你好"
测试结果:
Tips: 由于硬盘的大小是有限的 , 我们不可能等到所有数据都读取到硬盘中再去处理 , 通常是一边读取一边处理 , 如果不这么做读满硬盘后 , 下一轮数据会覆盖上一轮数据.
两种读取方式的比较:
第二次读取版本需要一个字节数组作为缓冲区 , 这样做的目的是提高IO操作的效率 , 单次IO操作需要访问硬盘IO设备 , 如果像第一次读取版本 , 频繁的访问IO设备 , 会耗时更多.因此如果能缩短IO的次数就能提高程序整体的效率.
- 第一个版本的代码是一次读取一个字节 , 循环的次数较高 , read的次数也很高.
- 第二个版本的代码是一次读取1024个字节 , 循环次数降低了很多 , read的次数也变少了.
3.3 OutputStream概述
修饰符及返回值类型 | 方法签名 | 说明 |
void | write(int b) | 写入要给字节的数据 |
void | write(byte[] b) | 将 b 个字符数组中的数据全部写入 os 中 |
void | write(byte[] b,int off, int len) | 将 b 这个字符数组中从off 开始的数据写入 os 中,一共写len个 |
void | close() | 关闭字节流 |
void | flush() | 刷新缓冲区 |
OutputStream 同样也是一个抽象类 , 要使用还需具体的实现类 , 我们此时只关心文件的读写 , 所以使用FileOutputStream.
代码测试:
public static void main(String[] args) throws IOException {
OutputStream outputStream = new FileOutputStream("./text.txt");
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
outputStream.close();
}
对于 OutputStream 来说 , 默认情况下 , 打开一个文件 , 会先清空文件的内容再进行写操作.
如何区分 InputSream和 OutputSream?
input 和 output的方向是以CPU为中心来判断的.
- 数据朝着CPU的方向流向 , 就是输入. 所以就把数据从硬盘读到内存这个过程称为 input.
- 数据远离CPU的方向流向 , 就是输出 , 所以把数据从内存到硬盘 , 这个过程称为 output.
close()操作的作用
close 操作的作用不仅仅是关闭文件释放资源还有刷新缓冲区.
在内核中使用PCB这样的数据结构来表示进程 , 一个线程对应一个PCB , 一个进程可对应一个也可对应多个PCB. PCB中有一个重要的属性 , 文件描述符表 , 相当于一个数组记录了该进程打开了哪些文件(如果一个进程里有多个线程多个PCB , 那么这些PCB共用一个文件描述符表) ,
每次打开文件操作 , 就会在文件描述符表中申请一个位置 , 把这个信息放进去. 每次关闭文件也会把这个文件描述符表对应的表项给释放掉.如果没有及时释放文件 , Java中虽然有垃圾回收机制 , 但这个垃圾回收机制并不一定及时 , 那么就意味着文件描述符表可能会被占满 , 占满之后再打开文件就会打开失败.
使用 try-with-resource 简化close 写法.
这个写法虽然没有显示的写 close , 实际上只要 try 语句执行完毕 , 就可以自动执行到 close.
try ( OutputStream outputStream = new FileOutputStream("./text.txt");){
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
}
Tips: 不是随便一个对象放入 try() 中就可以释放 , 得实现Closeable接口的类.
3.4 利用 Scanner 进行字符读取.
上述例子中我们可以看到对字符类型直接使用 InputStream 进行读取是非常困难的 , 所以使用 Scanner类可以更加简洁的完成该操作.
构造方法 | 说明 |
Scanner(InputStream is,String charset) | 使用 charset 字符集进行 is 的扫描读取 |
文件内容:
public static void main(String[] args) throws IOException {
try(InputStream inputStream = new FileInputStream("./text.txt")) {
Scanner scanner = new Scanner(inputStream,"UTF-8");
System.out.println(scanner.next());
}catch (IOException e){
e.printStackTrace();
}
}
4.小程序练习
示例一:
扫描指定目录 , 并找到名称中包含指定字符的所有普通文件(不包含目录) , 并且后续询问用户是否要删除该文件.
类似于如下操作:
public class ThreadDemo9 {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) throws IOException {
//让用户输入一个指定搜索目录
System.out.println("请输入搜索路径: ");
String basePath = scanner.next();
//针对用户输入进行简单判定
File root = new File(basePath);
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) {
System.out.println("[sanDir]" + root.getAbsolutePath());
// 1.列出当前路径下包含的内容
File[] files = root.listFiles();//相当于看了一下目录中有啥?
if (files == null) {
// 空目录
return;
}
// 2.遍历当前列出结果
for (File file : files) {
if (file.isDirectory()) {
// 如果是目录进一步递归
scanDir(file, nameToDelete);
} else {
if (file.getName().contains(nameToDelete)) {
System.out.println("确认是否要删除" + file.getAbsolutePath() + " 嘛?");
String choice = scanner.next();
if (choice.equals("y") || choice.equals("Y")) {
file.delete();
System.out.println("删除成功!");
} else {
System.out.println("删除失败!");
}
}
}
}
}
}
示例二
进行普通文件的复制
public static void main(String[] args) throws IOException {
// 输入两个路径
// 源 和 目标
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("您当前输入的源路径有误");
return;
}
File destFile = new File(destPath);
if (destFile.isFile()) {
//如果已存在也不能进行拷贝操作
System.out.println("您输入的目录路径有误");
return;
}
//进行拷贝操作
try (InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputstream = new FileOutputStream(destFile)) {
// 进行读文件操作
while (true) {
int b = inputStream.read();
if (b == -1) {
break;
}
outputstream.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
}
Tips:OutputStream 在写文件时如果文件不存在 , 就会自动创建 , 但 InputStream 不行会抛出异常.
示例三:
扫描指定目录 , 找到名称或内容中包含普通字符的所有文件
public class ThreadDemo11 {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要扫描的目录: ");
String dir = scanner.next();
File rootDir = new File(dir);
if(!rootDir.isDirectory()){
System.out.println("输入目录错误 退出!");
return;
}
System.out.println("请输入要找出的文件名中的字符");
String token = scanner.next();
List<File> ret = new ArrayList<>();
//因为文件是树形结构 , 所以我们使用深度优先遍历
scanDirWithContent(rootDir,token,ret);
System.out.println("共找到"+ret.size()+"个符合条件的文件,它们分别是: ");
for (File file:ret){
System.out.println(file.getCanonicalFile());
}
}
private static void scanDirWithContent(File rootDir, String token, List<File> ret) throws IOException {
File[] files = rootDir.listFiles();
if (files.length == 0||files == null){
return;
}
for (File file:files){
if(file.isDirectory()){
scanDirWithContent(file,token,ret);
}else{
if(isContentContain(file,token)){
ret.add(file.getAbsoluteFile());
}
}
}
}
private static boolean isContentContain(File file, String token) throws IOException {
StringBuilder sb = new StringBuilder();
try (InputStream is = new FileInputStream(file);
Scanner scanner = new Scanner(is)){
while (scanner.hasNextLine()){
sb.append(scanner.nextLine());
// sb.append("/r/n");
}
}
return sb.indexOf(token) !=-1;
}
}