【javaEE】文件操作--io
1.❤️❤️前言~🥳🎉🎉🎉
Hello, Hello~ 亲爱的朋友们👋👋,这里是E绵绵呀✍️✍️。
如果你喜欢这篇文章,请别吝啬你的点赞❤️❤️和收藏📖📖。如果你对我的内容感兴趣,记得关注我👀👀以便不错过每一篇精彩。
当然,如果在阅读中发现任何问题或疑问,我非常欢迎你在评论区留言指正🗨️🗨️。让我们共同努力,一起进步!
加油,一起CHIN UP!💪💪
🔗个人主页:E绵绵的博客
📚所属专栏:1. JAVA知识点专栏
深入探索JAVA的核心概念与技术细节
2.JAVA题目练习
实战演练,巩固JAVA编程技能
3.c语言知识点专栏
揭示c语言的底层逻辑与高级特性
4.c语言题目练习
挑战自我,提升c语言编程能力
5.Mysql数据库专栏
了解Mysql知识点,提升数据库管理能力
6.html5知识点专栏
学习前端知识,更好的运用它
7. css3知识点专栏
在学习html5的基础上更加熟练运用前端
8.JavaScript专栏
在学习html5和css3的基础上使我们的前端使用更高级、
9.JavaEE专栏
学习更高阶的Java知识,让你做出网站
📘 持续更新中,敬请期待❤️❤️
2.认识文件
文件路径
文件是对于"硬盘"数据的一种抽象,在一台计算机上,有非常多的文件,这些文件是通过 "文件系统" 来进行组织的,本质上就是通过 "目录"(文件夹) 这样的树形结构来组织文件的,画个图理解一下:
![]()
有了目录,我们就可以使用目录的层次结构来描述文件所在的位置,即 "路径"。如:"E:\cs2\5EClient",在这里还有两个概念:
绝对路径:以 c:d:盘符开头的路径,这种路径就是 "绝对路径"。
相对路径:需要指定一个目录作为基准目录,从基准目录出发,到达指定的文件,这里的路径就是 "相对路径",其中有三个特殊的符号:
.\
表示当前目录。
例如:
.\file.txt
指的是当前目录中的file.txt
文件。(注意 .\ 可以忽视不写)
\
表示下一级,用于分隔目录层级。
例如:
C:\Users\Username\Documents
中的\
用于分隔不同的目录层级,表示下一级
..\
表示上一级目录(父目录)。
例如:
..\file.txt
指的是上一级目录中的file.txt
文件。
这里给出一个相对路径示例:
.\Projects\..\Documents\Reports\2023\..\Summary\Final\report.txt
通过上述讲述规律我们可以简化为
Documents\Reports\Summary\Final\report.txt
如果你搞懂这个逻辑是可以很快把它简化出来的,搞不懂的话那就好好自己琢磨一下。
文件类型
文件主要分为两大类:
1.文本文件:文件中保存的内容必须要求都是合法字符(计算机存储的数据都是二进制的,能通过字符编码将二进制数据转换成字符的就是合法字符)
2.二进制文件:文件中保存的数据可以存在不合法的数据(计算机存储的数据都是二进制的,不能通过字符编码将二进制数据转换成字符的就是不合法字符)
区分文本文件和二进制文件:将文件直接使用记事本打开,如果是乱码,就是二进制文件,如果不是,就是文本文件。
但是也存在一种情况:用记事本打开二进制文件不存在乱码(二进制文件并不是必须要求存在不合法的数据),所以这种情况分辨不出来,我们就通过后缀名去分辨:
常见的文本文件后缀名包括:
.txt
最常见的纯文本文件格式,通常用于存储简单的文本内容。
示例:
notes.txt
.csv
逗号分隔值文件,用于存储表格数据。
示例:
data.csv
.html
超文本标记语言文件,用于网页内容。
示例:
index.html
.xml
可扩展标记语言文件,用于存储结构化数据。
示例:
config.xml
.json
JavaScript 对象表示法文件,用于存储和传输结构化数据。
示例:
settings.json
.log
日志文件,通常记录系统或应用程序的运行信息。
示例:
error.log
.md
Markdown 文件,用于编写格式化的文本。
示例:
README.md
常见的二进制文件后缀名包括:
.exe
可执行文件,通常用于 Windows 应用程序。
示例:
program.exe
.dll
动态链接库文件,包含可由多个程序共享的代码和数据。
示例:
library.dll
.jpg / .jpeg
图像文件,采用 JPEG 压缩格式。
示例:
photo.jpg
.png
便携式网络图形文件,支持无损压缩和透明背景。
示例:
image.png
.mp3
音频文件,采用 MPEG 音频压缩格式。
示例:
song.mp3
.mp4
视频文件,采用 MPEG-4 压缩格式。
示例:
video.mp4
.zip
压缩文件,用于存储多个文件或文件夹。
示例:
archive.zip
便携式文档格式文件,通常用于存储格式化文档。
示例:
document.pdf
.bin
通用的二进制文件,通常用于存储原始二进制数据。
示例:
data.bin
这里还有一个特殊的后缀名:.data 文件可以是二进制文件,也可以是纯文本文件,具体取决于创建它的程序。
3.Java中操作文件——File
FILE针对文件系统进行操作,如创建文件, 删除文件, 创建目录,重命名文件....
File的属性
E:\cs2\5EClient 中的 \ 就是 pathSeparator,如果当前的系统是 Windows,\ 或者 / 都可以作为分隔符,如果系统是 Linux 或 Mac ,只能使用 / 作为分隔符,一般我们都建议使用 / 作为分隔符,因为 \ 一般还需要搭配转义字符来使用,而且因为linux也是/,如果在windows中用/可以直接跨系统。
File的构造方法
以下是
File
类三种构造方法:
1.
File(String pathname)
直接通过相对路径或者绝对路径创建
File
实例。File file = new File("C:/Users/Username/Documents/example.txt");
2.
File(String parent, String child)
通过父目录路径和孩子文件路径创建
File
实例。File file = new File("C:/Users/Username/Documents", "example.txt");
3.
File(File parent, String child)
通过父目录
File
对象和孩子文件路径创建File
实例。File parentDir = new File("C:/Users/Username/Documents"); File file = new File(parentDir, "example.txt");
File的方法
以下是 File
类常用方法的简单示例:
1. getParent()
返回父目录路径。
File file = new File("C:/Users/Username/Documents/example.txt"); System.out.println("父目录: " + file.getParent()); // 输出: C:/Users/Username/Documents
2. getName()
返回文件或目录的名称。(文件包含后缀,目录没有后缀)
System.out.println("文件名称: " + file.getName()); // 输出: example.txt
3. getPath()
返回文件或目录的路径。(可能是相对路径,也可能是绝对路径,具体取决于文件对象是通过相对还是绝对创建的。)
System.out.println("文件路径: " + file.getPath()); // 输出: C:/Users/Username/Documents/example.txt
4. getAbsolutePath()
返回绝对路径。(不经修饰的)
System.out.println("绝对路径: " + file.getAbsolutePath()); // 输出:
C:\Users\Username\Documents\..\Downloads\.\example.txt(这就为不经修饰的路径)
5. getCanonicalPath()
返回修饰后的绝对路径。
System.out.println("规范路径: " + file.getCanonicalPath()); // 输出: C:/Users/Username/Downloads/example.txt(根据方法四的例子可知这为修饰后的路径,更易读了)
6. exists()
检查文件或目录是否存在。(对于file对象对应的路径可以是虚拟的,不在真实文件系统中存在)
System.out.println("文件是否存在: " + file.exists()); // 输出: true 或 false
7. isDirectory()
判断是否为目录。
System.out.println("是否是目录: " + file.isDirectory()); // 输出: true 或 false
8. isFile()
判断是否为普通文件。
System.out.println("是否是文件: " + file.isFile()); // 输出: true 或 false
9. createNewFile()
创建新文件。(要求File对象必须是文件,不能是目录。 并且对于该路径中的各级目录必须都存在,否则不能创建)
boolean created = file.createNewFile(); System.out.println("文件是否创建成功: " + created); // 输出: true 或 false
10. delete()
删除文件或目录。
boolean deleted = file.delete(); System.out.println("文件是否删除成功: " + deleted); // 输出: true 或 false
11. deleteOnExit()
标记文件,在 JVM 退出时删除,不会立刻删除。
file.deleteOnExit();
12. list()
返回目录下的文件名称和目录名称的数组。
String[] files = file.list(); for (String name : files) { System.out.println(name); }
13. listFiles()
返回目录下的 File
对象数组。
File[] files = file.listFiles(); for (File f : files) { System.out.println(f.getName()); }
14. mkdir()
创建单个目录。(尝试创建D: /path/to/singleDirectory
目录,如果中间目录不存在,则不能创建成功)
boolean created = file.mkdir(); System.out.println("目录是否创建成功: " + created); // 输出: true 或 false
15. mkdirs()
创建多级目录。(尝试创建D: /path/to/singleDirectory
目录,如果中间目录不存在,则把不存在的中间目录以及目标目录都创建出来)
boolean created = file.mkdirs(); System.out.println("目录是否创建成功: " + created); // 输出: true 或 false
16. renameTo(File dest)
重命名文件或目录。(要求两个File对象的父路径都必须一样)
File dest = new File("C:/Users/Username/Documents/newName.txt"); boolean renamed = file.renameTo(dest); System.out.println("文件是否重命名成功: " + renamed); // 输出: true 或 false
17. canRead()
检查文件是否可读。(文件会有可读可写权限)
System.out.println("文件是否可读: " + file.canRead()); // 输出: true 或 false
18. canWrite()
检查文件是否可写。(文件会有可读可写权限)
System.out.println("文件是否可写: " + file.canWrite()); // 输出: true 或 false
4.Java中文件内容读写——数据流
关于文件内容操作:读文件,写文件,打开文件,关闭文件。
在Java 中通过"流"(stream 流)这样的一组类,进行上述的文件内容操作。
数据流的分类
数据流根据文件类型也分成了两种:
1.字节流:对应二进制文件,每次读写的最小单位是 "字节"
2.字符流:对应文本文件,每次读写的最小单位是 "字符",英文的字符都是一个字节,一个汉字在不同的字符编码中是不同的字节大小,在 utf8 是 3 个字节,在 unicode 是 2 个字节。(字符流本质上是针对字节流进行的一层封装)
JAVA针对读写两种操作,分别为字节流提供了 InputStream(输入) 和 OutputStream(输出) 类,为字符流提供了 Reader(输入) 和 Writer(输出) 类。
这里有一个注意点,如何区分输入和输出:
以cpu为主视角,输入就是输入一些数据到cpu中(这些数据有可能是读取的数据),所以读取是输入流。
以cpu为主视角,输出就是将cpu中的数据放到别的地方(这些数据有可能是会放到硬盘中),所以写就是输出流。
字符流的读写
Reader
Reader是一个抽象类,我们需要用FileReader去实例化。
下面是构造方法:
FileReader(File file) 利用 File 构造文件输入流
FileReader(String name) 利用文件路径构造文件输入流
之后的三个类的构造方法我们就不讲了,跟这个一摸一样,套模板
1. int read()
读取一个字符,返回其 Unicode 编码(两个字节大小)。如果到达流的末尾,返回 -1
。
这里有个问题,为什么是返回unicode编码呢?不能是utf8编码呢?因为在 Java 标准库内部, 对于字符编码是进行了很多的处理工作的,如果是只使用 char,此时使用的字符集固定就是 unicode,如果是使用 String, 此时就会自动的把每个字符的 unicode 转换成 utf8。
那为什么字符串不能用Unicode呢?非要转换?当我们把多个 unicode 连续放到一起,是很难区分出从哪里到哪里是一个完整的字符的.而utf8 是可以做到区分的,所以字符串就是utf8编码,单个字符就是unicode编码。
2. int read(char[] cbuf)
读取多个字符,尽量填满 cbuf
数组,返回实际读取的字符数。如果到达流的末尾,返回 -1
。
特别注意:这个方法不会关心数组 cbuf 中是否已经存在数据,它会直接将读取的字符从数组的起始位置(索引 0)开始覆盖写入。所以如果上次读满了数组,下次再进行读写会直接覆盖上次读写的数据(注意这种类型的读方法都是直接覆盖的,之后再出现这种类似的读方法我就不再特别说明了,比如下面的方法)
3. int read(char[] cbuf, int off, int len)
从 off
下标开始,读取最多 len
个字符到 cbuf
数组,返回实际读取的字符数。如果到达流的末尾,返回 -1
。
Writer
它是一个抽象类,我们需要用FileWriter去实例化。
构造方法跟上面一样。
注:默认情况下,在创建文件输出流时会将文件中原有的内容清空,这是在你创建输出流对象时发生的,一旦文件输出流创建完成,你就可以多次调用
write
方法来写入数据,而不会影响文件中已有的内容(调用write后并不会将内容清空,只有创建输出流时才会清空)所以我们可以在构造方法最后一个参数里加一个 true , 在创建文件输出流时就不会将文件中原有的内容清空(适用于所有输出流)
字节流的读写
InputStream
同样用 fileinputstream 实例化对象,构造方法同理。
它跟reader几乎一个样,不同的是它返回的是一个字节,另一个是字符。
由于是一个字节,所以我们采用十六进制表达,返回出两位数,更加好观察比十进制。
outputstream
同样用 fileoutputstream 实例化对象,构造方法同理
import java.io.*;
public class Demo5 {
public static void main(String[] args) {
try(OutputStream outputStream = new FileOutputStream("d:/text.txt",true)){
String s = "你好世界";
outputStream.write(s.getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
它是输入流,所以同理,想要在该文件追加数据需要在构造方法后加个true。
close()方法
在讲完这四个经典流之后,还要注意一个问题:
当我们使用这些输入流输出流打开文件后,一个文件如果使用完了,要记得close,使用 close 方法,最主要的目的是为了释放文件描述符。
文件描述符我们之前在讲解pcb时讲过,每个打开的文件都有一个唯一的文件描述符,它存储在顺序表中,一个进程每次用输入流输出流打开一个文件,就需要在这个表里分配一个文件描述符,而这个数组的长度是存在上限的,如果你的代码,一直打开文件,而不去关闭文件,就会使这个表里的文件描述符,越来越多,一直到把这个数组占满了,后续再尝试打开其他文件,就会出错了,导致系统崩溃,这也叫做文件资源泄露,非常类似于内存泄露。
那么我们的close方法应该放在哪里呢?正常随便放置吗? 如下代码
import java.io.FileReader;
public class SimpleFileReaderExample {
public static void main(String[] args) {
try {
FileReader reader = new FileReader("example.txt");
int data;
while ((data = reader.read()) != -1) {
System.out.print((char) data);
reader.close();
}
} catch (Exception e) {
System.out.println("发生错误: " + e.getMessage());
}
}
}
}
看到这个代码中带有捕获异常,我们额外说一点,对于文件io读写的代码通常都会有很多受查异常,所以一般在文件io代码中都要处理ioException异常,要么throws,要么trycatch,否则编译不成功,跟线程代码一样,通常都要处理异常。
那么回归正题,如果这么写,一旦在还没close时抛出异常或者系统崩溃,那么就执行不了close,就可能导致问题发生,所以我们都是将它放在finally中。
import java.io.FileReader;
public class SimpleFileReaderExample {
public static void main(String[] args) {
try {
FileReader reader = new FileReader("example.txt");
int data;
while ((data = reader.read()) != -1) {
System.out.print((char) data);
}
} catch (Exception e) {
System.out.println("发生错误: " + e.getMessage());
} finally {
reader.close();
}
}
}
}
但这看起来还是不太美观,为此我们引用了一个try的语法,将代码变为如下代码:
import java.io.FileReader; public class SimpleFileReaderExample { public static void main(String[] args) { try(FileReader reader = new FileReader("example.txt")) { int data; while ((data = reader.read()) != -1) { System.out.print((char) data); } } catch (Exception e) { System.out.println("发生错误: " + e.getMessage()); } } } }
它是try-with-resources 语句,这是一种更简洁且自动管理资源(如文件流、网络连接、数据库连接等)的方法。
try (ResourceType resource = new Resource()) { // 使用资源的代码 } catch (ExceptionType e) { // 异常处理代码 }
这里要求resource必须实施closable接口(有close方法),当用了该语法后,我们就不用搞一个finally里面包含close的代码,这语法里面自动隐藏包含着finally里面包含close的代码,可以达到一样的效果,并且更优美,以后我们写的类似数据流代码都是跟该格式一样。
Scanner
除了用我们已知的的read方法读取输入流中的字符,我们还可以用Scanner去读取输入流里的文本数据。
public class Demo6 { public static void main(String[] args) { try (FileReader reader = new FileReader("d:/a.txt")){ Scanner scanner = new Scanner(reader); String s = scanner.next(); System.out.println(s); } catch (IOException e) { throw new RuntimeException(e); } } }
从这个scanner语法结构我们发现一个很重要的事,对于之前的scanner(system.in)也是一样的语法结构,没错其实system.in也是一个输入流,通过用户键盘去输入数据,而后通过scanner读取system.in输入流的数据。
printwiter
除了通过write去写外,我们还可以通过printwiter去操作:
public class Demo7 { public static void main(String[] args) { try(OutputStream outputStream = new FileOutputStream("d:/a.txt")){ PrintWriter writer = new PrintWriter(outputStream); writer.println("你好世界"); } catch (IOException e) { throw new RuntimeException(e); } } }
在使用了之后,我们却发现并没有添加成功,这是为什么呢?
因为 PrintWriter 这个类,在进行写入操作的时候,不一定直接写入硬盘,而是先把数据写入一个内存中的空间,叫做 "缓冲区"。为什么会出现缓冲区?因为把数据写入内存,是非常快的,而把数据写入硬盘,是非常慢的(比内存慢几千倍甚至更多),为了提高效率,我们选择降低写硬盘的次数,等积累多了之后,再一次写进去。这样就会出现问题,我们将数据写入 "缓冲区" 后,还没有将缓冲区的数据写入硬盘,进程就结束了,此时数据就丢失了,也就会出现上述图片中的问题。
解决方案是什么呢?确保数据能完整的写入硬盘,我们需要手动的用 flush() 方法刷新缓冲区:
public class Demo7 { public static void main(String[] args) { try(OutputStream outputStream = new FileOutputStream("d:/a.txt")){ PrintWriter writer = new PrintWriter(outputStream); writer.println("hello world"); writer.flush(); } catch (IOException e) { throw new RuntimeException(e); } } }
以后我们出现类似情况就有可能是缓存区遗留了信息,没及时传上去,要记得用flush及时刷新缓存区。
那么缓冲区会在我们基本的四个类里出现吗?
不会,它出现在BufferedReader和BufferedWriter中,printwiter之所以有缓冲区是因为底层是BufferedWriter类,所以才有缓冲区。