一,认识文件
1.1 树形结构组织和目录
文件是对于"硬盘"数据的一种抽象,在一台计算机上,有非常多的文件,这些文件是通过 "文件系统" 来进行组织的,本质上就是通过 "目录"(文件夹) 这样的树形结构来组织文件的,画个图理解一下:
有了目录,我们就可以使用目录的层次结构来描述文件所在的位置,即 "路径"。如:D:\Program Files (x86)\编程3\Common\VSPerfCollectionTools\vs2022\1033,在这里还有两个概念:
- 绝对路径:以 C:D:盘符开头的,这种路径就是 "绝对路径"。
- 相对路径:需要指定一个目录作为基准目录,从基准目录出发,到达指定的文件,这里的路径就是 "相对路径"。这些路径往往是以 . (代表当前目录) 或者 .. (代表当前目录的上一级目录) 开头的。
1.2 文件类型
文件主要分为两大类:
1)文本文件:文件中保存的数据都是字符串,保存的内容都是合法字符(计算机存储的数据都是二进制的,能通过字符编码将二进制数据转换成字符的就是合法字符)
2)二进制文件:文件中保存的数据是二进制数据,即不是合法的字符
区分文本文件和二进制文件:将文件直接使用记事本打开,如果是乱码,就是二进制文件,如果不是,就是文本文件。
二,文件操作 - FILE
2.1 属性
修饰符及属性 | 属性 | 说明 |
static String | pathSeparator | 依赖于系统的路径分隔符,String 类型的表示 |
static char | pathSeparator | 依赖于系 统的路径分隔符,String 类型的表示 |
E:\01\MSDN 中的 \ 就是 pathSeparator,如果当前的系统是 Windows,\ 或者 / 都可以作为分隔符,如果系统是 Linux 或 Mac ,只能使用 / 作为分隔符,一般建议使用 / 作为分隔符,因为 \ 一般还需要搭配转义字符来使用。
2.2 构造方法
构造方法 | 说明 |
File(File parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者 相对路径 |
File(String parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
2.3 方法
返回值类型 | 方法名 | 说明 |
String | getParent() | 返回 File 对象的父目录文件路径 |
String | getName() | 返回 FIle 对象的纯文件名称 |
String | getPath() | 返回 File 对象的文件路径 |
String | getAbsolutePath() | 返回 File 对象的绝对路径 |
String | getCanonicalPath() | 返回 File 对象的修饰过的绝对路径 |
boolean | exits() | 判断 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(File dest) | 进行文件改名,也可以视为我们平时的剪切、粘贴操 作 |
boolean | canRead() | 判断用户是否对文件有可读权限 |
boolean | canWirte() | 判断用户是否对文件有可写权限 |
public class Demo {public static void main(String[] args) throws IOException {File file = new File("./text.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());}
}
import java.io.File;
import java.io.IOException;
import java.util.Arrays;public class Demo1 {public static void main(String[] args) throws IOException {File file = new File("d:/text.txt");System.out.println(file.exists());//falseSystem.out.println(file.isDirectory());//falseSystem.out.println(file.isFile());//falseSystem.out.println(file.createNewFile());//trueSystem.out.println(file.delete());//true//file.deleteOnExit();在程序全部执行完之后删除文件File file1 = new File("d:/");String[] ret = file1.list();System.out.println(Arrays.toString(ret));File file2 = new File("d:/aaa/bbb/ccc");boolean ans = file2.mkdirs();//能创建多级目录//file2.mkdir();只能创建一级目录,如 d:/aaaSystem.out.println(ans);}
}
三,文件内容读写 - 数据流
数据流根据文件类型也分成了两种:
1)字节流:对应二进制文件,每次读写的最小单位是 "字节"
2)字符流:对应文本文件,每次读写的最小单位是 "字符",英文的字符都是一个字节,一个汉字在不同的字符编码中是不同点大小,在 utf8 是 3 个字节,在 unicode 是 2 个字节。(字符流本质上是针对字节流进行的一层封装)
JAVA针对读写两种操作,分别为字节流提供了 InputStream(输入) 和 OutputStream(输出) 类,为字符流提供了 Reader(输入) 和 Writer(输出) 类。这里有一个注意点,如何区分输入和输出,画个图:
3.1 字符流 - Reader
返回值类型 | 方法名 | 说明 |
int | read() | 从文件中读取一个字符,返回unicode编码 |
int | read(char[] cbuf) | 从文件中读取若干字符,将cbuf数组填满,返回实际读取的字符数 |
int | read(chae[] cbuf, int off, int len) | 从文件中读取作干字符,从off下标开始,长度为len的cbuf数组填满,返回实际读取的字符数 |
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;public class Demo3 {public static void main(String[] args) throws IOException {//一次读一个字符Reader reader = new FileReader("d:/text.txt");//打开文件while(true){int n = reader.read();//读取一个字符if(n == -1){//返回-1表示文件读取完毕break;}char ch = (char) n;System.out.println(n);}reader.close();}
}
但是这么写还是可能会出现文件资源泄露,如果在while循环中抛出异常,下面的close()方法就执行不到了,所以我们可以使用 try...finally..来实现:
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;public class Demo3 {public static void main(String[] args) throws IOException {//一次读多个字符Reader reader = new FileReader("d:/text.txt");//打开文件try{while(true){char[] ret = new char[10];int n = reader.read(ret);if(n == -1) break;for (int i = 0; i < n; i++) {System.out.println(ret[i]);}}}finally {reader.close();//关闭操作}}
}
这么写虽然解决了问题,但是不够方便,在这里还有一种写法:
public class Demo3 {public static void main(String[] args) throws IOException { //只有实现closeable接口才可以这样写(流对象都可以)try(Reader reader = new FileReader("d:/text.txt")){while(true){char[] ret = new char[10];int n = reader.read(ret);if(n == -1) break;for (int i = 0; i < n; i++) {System.out.println(ret[i]);}}}}
}
3.2 字符流 - Writer
方法名 | 说明 |
write(int c) | 一次写一个字符 |
write(String str) | 一次写多个字符 |
write(char[] cbuf) | 一次写多个字符,使用字符数组 |
write(String str, int off, int len) | 从下标off开始往文件中写入,长度为len |
write(char[] cbuf, int off, int len) | 从下标off开始往文件中写入,长度为len |
注:默认情况下,写入文件会将文件中的原有内容清空。
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;public class Demo4 {public static void main(String[] args) {try(Writer writer = new FileWriter("d:/text.txt")) {writer.write("原神,启动!");//写入,先清空再写入} catch (IOException e) {throw new RuntimeException(e);}/* 在构造方法参数中加一个 true , 就可以直接在文件后面填写,不需要清空try(Writer writer1 = new FileWriter("d:/text.txt",true)) {writer1.write("原神,启动!");//写入} catch (IOException e) {throw new RuntimeException(e);}*/}
}
3.3 字节流 - 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() | 关闭字节流 |
import java.io.*;public class Demo5 {public static void main(String[] args) {try(InputStream inputStream = new FileInputStream("d:/text.txt")) {byte[] buffer = new byte[10];while (true){int n = inputStream.read(buffer);if(n == -1) break;for (int i = 0; i < n; i++) {System.out.printf("%x\n",buffer[i]);}}}catch (IOException e) {throw new RuntimeException(e);}}
}
3.4 字节流 - OutputStream
返回值类型 | 方法名 | 说明 |
void | write() | 写入要给字节的数据 |
void | write(byte[] b) | 将 b 这个字符数组中的数据全部写入 |
int | write (byte[] b, int off, int len) | 将 b 这个字符数组中从 off 开始的数据写入 ,一共写 len 个 |
void | close() | 关闭字节流 |
void | flush() | 大多的 OutputStream 为 了减少设备操作的次数,在写数据的时候都会将数据先暂时写入内存的 一个指定区域里,直到该区域满了或者其他指定条件时才真正将数据写 入设备中,这个区域一般称为缓冲区。但造成一个结果,就是我们写的 数据,很可能会遗留一部分在缓冲区中。需要在最后或者合适的位置, 调用 flush (刷新操作,将数据刷到设备中。 |
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);}}
}
3.5 字节流转字符流
当别人传给你的是一个字节流文件,但是你知道实际数据内容是文本数据时,我们可以通过以下方法来实现转换:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class Demo6 {public static void main(String[] args) {try (InputStream inputStream = new FileInputStream("d:/text.txt")){Scanner scanner = new Scanner(inputStream);String s = scanner.next();System.out.println(s);} catch (IOException e) {throw new RuntimeException(e);}}
}
import java.io.*;public class Demo7 {public static void main(String[] args) {try(OutputStream outputStream = new FileOutputStream("d:/text.txt")){PrintWriter writer = new PrintWriter(outputStream);writer.println("fsaf"); } catch (IOException e) {throw new RuntimeException(e);}}
}
因为 PrintWriter 这个类,在进行写入操作的时候,不一定时直接写入硬盘,而是先把数据写入一个内存中的空间,叫做 "缓冲区"。为什么会出现缓冲区?因为把数据写入内存,是非常快的,而把数据写入硬盘,是非常慢的(比内存慢几千倍甚至更多),为了提高效率,我们选择降低写硬盘的次数。这样就会出现问题,我们将数据写入 "缓冲区" 后,还没有将缓冲区的数据写入硬盘,进程就结束了,此时数据就丢失了,也就会出现上述图片中的问题。
为了解决该问题,确保数据能完整的写入硬盘,我们需要手动的用 flush() 方法刷新缓冲区:
import java.io.*;public class Demo7 {public static void main(String[] args) {try(OutputStream outputStream = new FileOutputStream("d:/text.txt")){PrintWriter writer = new PrintWriter(outputStream);writer.println("fsaf");writer.flush();} catch (IOException e) {throw new RuntimeException(e);}}
}