目录
1.文件系统概述
1.1 认识文件
我们先来认识狭义上的文件(file)。针对硬盘这种持久化存储的I/O设备,当我们想要进行数据保存时, 往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概念,就类似办公桌上的一份份真实的文件一般。
文件除了有数据内容之外,还有一部分信息,例如文件名、文件类型、文件大小等并不作为文件的数据 而存在,我们把这部分信息可以视为文件的元信息。
1.2 树型结构组织和目录
随着文件越来越多,对文件的系统管理也被提上了日程,如何进行文件的组织呢,一种合乎自然 的想法出现了,就是按照层级结构进行组织 —— 也就是我们数据结构中学习过的树形结构。这样,一 种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹(folder)或者目录(directory)的 概念。
比如上面这种就是一种典型的树形结构组织。
1.3 文件路径(Path)
如何在文件系统中如何定位我们的一个唯一的文件就成为当前要解决的问题,从树型结构的角度来看,树中的每个结点都可以被一条从根开始,一直到达的结点的路径所描 述,而这种描述方式就被称为文件的绝对路径(absolute path)。(类似于我们之前学过的二叉树)
比如这就是一个绝对路径。
除了可以从根开始进行路径的描述,我们可以从任意结点出发,进行路径的描述,而这种描述方式就被称为相对路径(relative path),相对于当前所在结点的一条路径。
关于这两个路径的区别,大家可参照物理学上相对位移和绝对位移,主要就是参照点不同。
2.Java中操作文件
2.1 File概述和简单使用
Java 中通过 java.io.File 类来对一个文件(包括目录)进行抽象的描述。注意,有 File 对象,并不 代表真实存在该文件。
无论是我们这里提到的File对象,还是我们后面所提到的InputStream对象等,它们都是因为不方便直接操作硬盘里面的数据,才在内存里构造一个和它相关联的对象,操作这个对象就相当于操作硬盘数据。
就类似于我们日常生活中所使用的遥控器,虽然我们也可以直接对家电等直接进行操作,但因为不方便,所以我们才会使用遥控器。
我们先简单了解一下File类的基础信息
方法
我们简单看一下下面几段代码:
观察 get 系列的特点和差异
public class Demo1 {
public static void main(String[] args) throws IOException {
File file = new File("./hello-world.txt"); // 并不要求该文件真实存在
System.out.println(file.getParent());
System.out.println(file.getName());
System.out.println(file.getPath());
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalPath());
}
}
普通文件的创建
public class Demo1 {
public static void main(String[] args) throws IOException {
// 前面没写 ./ , 也相当于是 ./, ./ 可以省略~~
File file = new File("helloworld.txt");
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
System.out.println("========================");
// 创建文件
file.createNewFile();
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
}
}
普通文件删除
public static void main(String[] args) throws InterruptedException, IOException {
// 文件删除
File file = new File("helloworld.txt");
System.out.println(file.exists());
System.out.println("=======================");
file.delete();
System.out.println(file.exists());
}
创建文件目录
public class Demo1 {
public static void main(String[] args) {
File file = new File("test/aa/1");
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println("=========================");
// file.mkdir();
file.mkdirs();
System.out.println(file.exists());
System.out.println(file.isDirectory());
}
}
这里需要注意一下两个方法的区别,在上面的代码我们如果使用mkdir方法,虽然程序不会报错,但是目录并不会被创建。
文件重命名
public class Demo {
// 文件重命名
public static void main(String[] args) {
File file1 = new File("./test1.txt");
File file2 = new File("./test2.txt");
file1.renameTo(file2);
}
}
2.2 文件内容的读写 —— 数据流
在Java中关于文件的读写,提供了一些类
第一组: InputStream OutputStream 字节流,以字节为单位,一般用来操作二进制文件
第二组: Reader Writer 字符流,以字符为单位,一般用来操作文本文件
关于流的概念我们可以通过下图简单了解一些
注意:InputStream/OutputStream不能直接实例化,需要用到子类FileInputStream/FileOutputstream来读/写文件
2.2.1 简单字节文件的读取
我们在打开文件后就得到了InputStream对象,后续针对文件的操作都是通过InputStream展开的
在我们执行完一系列操作后一定要记得关闭文件,释放资源
为什么需要这样做?
首先我们需要知道,我们释放的资源主要是文件描述符表,他是用来存储当前进程打开的文件,每次打开文件都会占据文件描述符表的一个位置,而位置是存在上限的,如果一个进程一直在打开文件,没有释放,此时会导致我们后续进程在打开文件时失败
下面我们看到两段代码:
public class Demo1 {
public static void main(String[] args){
try(InputStream inputStream=new FileInputStream("test.txt")){
while(true){
//读取文件过程
int b=inputStream.read();
//读到文件尾退出
if(b==-1){
break;
}
System.out.printf("%c",b);
}
}catch(IOException e){
e.printStackTrace();
}
}
}
大家可能注意到,我们这段代码好像并没有关闭文件资源,但实际上我们是为了代码的简洁采用了 try-with-resources语法来简化我们的代码,使用方法如下:
try-with-resources 是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch 语句块中使用的资源。所谓的资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources 语句确保了每个资源在语句结束时关闭。所有实现了 java.lang.AutoCloseable 接口(其中,它包括实现了 java.io.Closeable 的所有对象),可以使用作为资源。也就是说在try执行完后()资源就会自动关闭,不需要我们再去处理。
public class Demo1 {
public static void main(String[] args){
try(InputStream inputStream=new FileInputStream("test.txt")){
byte[] buf=new byte[1024];
int len;
while (true) {
len = inputStream.read(buf);
if (len == -1) {
// 代表文件已经全部读完
break;
}
for (int i = 0; i < len; i++) {
System.out.printf("%c", buf[i]);
}
}
//循环输入输出,避免文件过大一次读不完
}catch(IOException e){
e.printStackTrace();
}
}
}
其实这两段代码干的事情是一样的,但是第二段代码是将读取的结果存储到byte数组内然后再去输出,这样IO次数更少,效率更高。(采用byte类型数组是因为read方法向数组内存入的数据类型就是byte,而采用while循环读入和for循环输出是因为防止文件过大数组不能一次存储完全部数据)
我们把结果放出来给大家展示一下:
2.2.2 利用 Scanner 进行字符读取
以上我们都是采用字节流来读取简单的字节文件,那么假如读取中文怎么办呢?
假如我们的文本为“你好中国”,用之前的代码来读会出现以下结果
这是因为了这几个中文的 UTF-8 编码后长度刚好是 3 个字节,所以直接这样读是行不通的,那我们还能用字节流来读取吗?答案是可以的。看到下面一段代码。
public class Demo2 {
public static void main(String[] args) throws IOException {
try (InputStream is = new FileInputStream("test.txt")) {
byte[] buf = new byte[1024];
int len;
while (true) {
len = is.read(buf);
if (len == -1) {
// 代表文件已经全部读完
break;
}
// 每次使用 3 字节进行 utf-8 解码,得到中文字符
// 利用 String 中的构造方法完成
// 这个方法了解下即可,不是通用的解决办法
for (int i = 0; i < len; i += 3) {
String s = new String(buf, i, 3, "UTF-8");
System.out.printf("%s", s);
}
}
}
}
}
的确能够成功输出,但是并不是通用的解决方法,我们继续优化。
上述例子中,我们看到了对字符类型直接使用 InputStream 进行读取是非常麻烦且困难的,所以,我 们使用一种我们之前比较熟悉的类来完成该工作,就是 Scanner 类。
public class Demo2 {
public static void main(String[] args) throws IOException {
try(InputStream inputStream=new FileInputStream("test.txt")){
Scanner scanner=new Scanner(inputStream);
String s=scanner.nextLine();
System.out.println(s);
}
}
}
于是我们得到了上面一段非常简短的代码,Scanner的用法就参照平时即可。
当然由于中文是一个个字符,那么我们使用Reader(字符流)自然也是可以的,代码也比较简单
public class Demo2 {
// 使用字符流读一下文件
public static void main(String[] args) throws IOException {
//由于已知文本较短就没有使用while来读入,这里注意
Reader reader = new FileReader("test.txt");
char[] buffer = new char[1024];
int len = reader.read(buffer);
for (int i = 0; i < len; i++) {
System.out.println(buffer[i]);
}
reader.close();
}
}
2.2.3 OutputStream 概述
说明
OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中, 所以使用 FileOutputStream
利用 OutputStreamWriter 进行字符写入
以下给出几段代码示例
public class Main {
public static void main(String[] args) throws IOException {
try (OutputStream os = new FileOutputStream("output.txt")) {
os.write('H');
os.write('e');
os.write('l');
os.write('l');
os.write('o');
// 不要忘记 flush
os.flush();
}
}
}
public class Main {
public static void main(String[] args) throws IOException {
try (OutputStream os = new FileOutputStream("output.txt")) {
String s = "Nothing";
byte[] b = s.getBytes();
os.write(b);
// 不要忘记 flush
os.flush();
}
}
}
public class Main {
public static void main(String[] args) throws IOException {
try (OutputStream os = new FileOutputStream("output.txt")) {
String s = "你好中国";
byte[] b = s.getBytes("utf-8");
os.write(b);
// 不要忘记 flush
os.flush();
}
}
}
2.2.4 利用 PrintWriter输出
上述,我们其实已经完成输出工作,但总是有所不方便,我们接来下将 OutputStream 处理下,使用 PrintWriter 类来完成输出
因为 PrintWriter 类中提供了我们熟悉的 print/println/printf 方法
OutputStream os = ...;
OutputStreamWriter osWriter = new OutputStreamWriter(os, "utf-8"); // 告诉它,我
们的字符集编码是 utf-8 的
PrintWriter writer = new PrintWriter(osWriter);
// 接下来我们就可以方便的使用 writer 提供的各种方法了
writer.print("Hello");
writer.println("你好");
writer.printf("%d: %s\n", 1, "没什么");
// 不要忘记 flush
writer.flush();
public static void main(String[] args) throws IOException {
try (OutputStream outputStream = new FileOutputStream("test2.txt")) {
// 此处的 PrintWriter 的用法就和 System.out 是一样的了
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println("hello");
printWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
上面那段代码就是利用PrintWriter来将文本输出到了“test2.txt”文件中,当然别忘了flush操作。
2.2.5 小程序练习
扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),并且后续询问用户是否要 删除该文件
public class Demo2 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要扫描的路径: ");
String rootPath = scanner.next();
File root = new File(rootPath);
if (!root.exists()) {
System.out.println("您输入的路径不存在, 无法进行扫描!");
return;
}
System.out.println("请输入要删除的文件名(或者一部分): ");
String toDelete = scanner.next();
// 准备进行递归, 通过递归的方式, 来找到所有的文件.
// 找到所有的文件之后, 再尝试进行删除
scanDir(root, toDelete);
}
public static void scanDir(File rootDir, String toDelete){
File[] files=rootDir.listFiles();//当前目录下所有文件
if(files==null){
// 空目录, 直接返回
return;
}
for(File f:files){
if(f.isDirectory()){
//是目录则继续递归
scanDir(f,toDelete);
}else{
//普通文件
tryDelete(f, toDelete);
}
}
}
public static void tryDelete(File f, String toDelete){
// 看看当前文件名是否包含了 toDelete, 如果包含, 就删除, 否则就啥都不干
if(f.getName().contains(toDelete)){
try {
System.out.println("是否要删除文件(Y/n): " + f.getCanonicalPath());
Scanner scanner = new Scanner(System.in);
String choice = scanner.next();
if (choice.equals("Y")) {
f.delete();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
进行普通文件的复制
public class Demo3 {
public static void main(String[] args) {
// 准备工作
Scanner scanner=new Scanner(System.in);
System.out.println("请输入要复制的文件路径: ");
String srcPath = scanner.next();
File srcFile = new File(srcPath);
if(!srcFile.exists()){
System.out.println("要复制的文件不存在!");
return;
}
if (!srcFile.isFile()) {
System.out.println("要复制的不是普通文件!");
return;
}
System.out.println("请输入要复制到的目标路径: ");
String destPath = scanner.next();
File destFile=new File(destPath);
if (destFile.exists()) {
System.out.println("要复制到的目标已经存在! ");
return;
}
// 进行拷贝工作
try (InputStream inputStream=new FileInputStream(srcFile)){
try(OutputStream outputStream=new FileOutputStream(destFile)){
byte[] buf=new byte[1024];
while(true){
int len= inputStream.read(buf);
System.out.println("len="+len);
if(len==-1){
System.out.println("拷贝完成");
break;
}
outputStream.write(buf,0,len);//从buf数组获取数据写入目标路径
}
}
}catch (IOException e){
e.printStackTrace();
}
System.out.println("复制完成");
}
}