文件
文件:在这里我们讨论的文件指的是硬盘上的文件。文件除了有保存的数据内容之外,还有部分信息,如文件名、文件类型等不作为文件的数据而存在,这部分信息可视为文件的元信息。
文件的分类:一般将文件划分为文本文件(保存字符集编码的文本)和二进制文件(按照标准格式保存的非被字符集编码过的文件)
文本文件:按照字符串的方式来理解文件内容(文本文件里的二进制内容,都是表示的字符串),即文本文件中的内容,都是合法的字符(即字符编码中的字符)
二进制文件:二进制文件没有文本文件上述限制,可以存储任何数据
如何判断一个文件是文本文件还是二进制文件?
我们可以借助记事本来判断,使用记事本打开一个文件,若看到的是正常的内容,则这个文件是文本文件;若是乱码,则是二进制文件
树形结构组织文件:文件系统是按照层级结构来组织文件的(也就是数据结构中学过的树形结构),这里的树是N叉树,每一个目录文件中可以有很多个子节点,而每一个普通文件(非目录文件)是一个叶子节点。
路径:操作系统中使用“路径”来描述一个具体文件的位置。例如:C:\Users\data\test.txt,表示从根节点出发(Windows系统是从盘符出发),一级一级往下走,直到找到目标文件,将中间经过的所有目录的名字串联起来,使用 / 或 \ 分隔
路径分隔符:分隔符的作用类似于标点符号,用来分隔路径列表中的文件名。上述C:\Users\data\test.txt中的\就是路径分隔符
在Linux中使用的路径分隔符是斜杠 /,而在windows中既可以使用 反斜杠 \ 也可以使用 /。由于在Java中,\ 是一个特殊的字符,被作为转义字符使用,因此,若我们使用 \ 分隔路径时,要使用 反斜杠符(\\)来表示字面意义上的 \ ,即(C:\\Users\\data\\test.txt)
路径的表示方式:
路径有两种风格的表示方式:
绝对路径:从树根节点出发(Windows则是从盘符开始)一层一层到目标文件(如C:\Users\data\test.txt)
相对路径:以当前所在目录为依据,找到目标文件。使用 . 表示当前所在目录,(当前在data目录,则 .\test.txt),..表示上层目录,(若目标文件为:C:\Users\t\data.txt,则相对路径为../t/data.txt)
Java中的文件操作主要分为两类:
1. 针对文件系统的操作,如创建文件、删除文件等
2. 针对文件内容的操作,如读文件、写文件等
Java中针对文件系统的操作,使用 File 类来对一个文件(包括目录)进行抽象的描述(有File对象,并不代表该文件真实存在),这个类在 java.io 包中
io:即 input(输入)和 output(输出)
以CPU为参照,数据从 硬盘 到 CPU 叫做 输入,从 CPU 到 硬盘 叫做 输出
针对文件系统的操作
针对文件系统的操作通常是通过 File 类来实现的
File
我们通过学习File类中的常见属性、构造方法和方法来了解 File 类
属性:
修饰符及类型 | 属性 | 说明 |
static String | pathSeparator | 依赖于系统的路径分隔符,String类型的表示 |
static String | pathSeparator | 依赖于系统的路径分隔符,char类型的表示 |
使用的路径分隔符根据系统自动调整
构造方法:
构造方法 | 说明 |
File(File parent, String child) | 通过父目录 + 孩子文件路径,创建一个File实例 |
File(String pathname) | 通过文件路径创建一个File实例,路径可以是绝对路径也可以是相对路径 |
File(String parent, String child) | 通过父目录 + 孩子文件路径,创建一个File实例,父目录用路径表示 |
方法:
返回值类型 | 方法 | 说明 |
String | getParent() | 返回 File 对象的父目录文件路径 |
String | getName() | 返回 File 对象的纯文件名称 |
String | getPath() | 返回 File 对象的文件路径 |
String | getAbsolutePath() | 返回 File 对象的绝对路径 |
String | getCanonicaPath() | 返回 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(File dest) | 进行文件改名 |
boolean | canRead() | 判断用户是否对文件有可读权限 |
boolean | canWrite() | 判断用户是否对文件有可写权限 |
我们先观察不同get方法的特点及它们之间的差异:
以相对路径的方式创建File对象:
package IO; import java.io.File; import java.io.IOException; public class Demo1 { public static void main(String[] args) throws IOException { File file = new File("./test");//并不要求该文件真实存在 System.out.println(file.getParent());//父目录文件路径 System.out.println(file.getName());//纯文件名 System.out.println(file.getPath());//文件路径 System.out.println(file.getAbsoluteFile());//绝对路径 System.out.println(file.getCanonicalFile());//File对象修饰过的绝对路径 } }
运行结果:
以绝对路径的方式创建File对象:
package IO; import java.io.File; import java.io.IOException; public class Demo1 { public static void main(String[] args) throws IOException { File file = new File("C:\\Users\\whisper\\Desktop\\code\\Java\\java\\test_1_21\\test");//并不要求该文件真实存在 System.out.println(file.getParent());//父目录文件路径 System.out.println(file.getName());//纯文件名 System.out.println(file.getPath());//文件路径 System.out.println(file.getAbsoluteFile());//绝对路径 System.out.println(file.getCanonicalFile());//File对象修饰过的绝对路径 } }
运行结果:
通过观察上述运行结果我们发现:当使用绝对路径创建 File对象时,其文件路径、绝对路径和File修饰过的绝对路径相同;而使用相对路径创建 File 对象时,则不同,文件路径表示创建的相对路径,绝对路径与 File 对象修饰过的绝对路径相比,包含.\
普通文件的创建:
package IO; import java.io.File; import java.io.IOException; public class Demo2 { public static void main(String[] args) throws IOException { File file = new File("./test.txt"); System.out.println(file.exists());//判断文件是否存在 System.out.println(file.isFile());//判断文件是否是普通文件 System.out.println(file.isDirectory());//判断文件是否是目录 boolean ret = file.createNewFile();//根据 File 对象,自动创建一个空文件 System.out.println(ret); } }
运行结果:
文件不存在,因此其既不是普通文件,也不是目录。正是由于文件不存在,因此可以根据 File 对象自动创建一个空文件(当文件存在时,则不能创建,不存在两个相同路径的文件)。
普通文件的删除:
package IO; import java.io.File; public class Demo3 { public static void main(String[] args) { File file = new File("./test.txt"); boolean ret = file.delete();//删除文件 System.out.println(ret); } }
运行结果为:true
当我们在运行Demo2时创建了一个test.txt文件,因此该文件存在,能够删除,也可以使用deleteOnExit()方法,在JVM运行结束时删除该文件
目录的创建:
package IO; import java.io.File; public class Demo4 { public static void main(String[] args) { File file = new File("./a/b/c"); boolean ret1 = file.mkdir(); boolean ret2 = file.mkdirs(); System.out.println(ret1); System.out.println(ret2); } }
运行结果:
使用mkdik()方法时,若中间目录不存在,则无法成功创建目录,而mkdirs()则可创建多级目录
目录下的所有文件:
package IO; import java.io.File; import java.util.Arrays; public class Demo5 { public static void main(String[] args) { File file = new File("."); File[] files = file.listFiles();//以 File 对象表示 System.out.println(Arrays.toString(files)); String[] strings = file.list(); System.out.println(Arrays.toString(strings)); } }
运行结果:
文件重命名:
package IO; import java.io.File; public class Demo6 { public static void main(String[] args) { File file = new File("test.txt");//要重命名的文件必须存在 File dest = new File("dest.txt");//更改后的文件名不能存在 System.out.println("file:" + file.exists()); System.out.println("dest:" + dest.exists()); System.out.println(file.renameTo(dest)); System.out.println("file: " + file.exists()); System.out.println("dest:" + dest.exists()); } }
运行结果:
针对文件内容的操作
在学习针对文件内容的操作之前,我们首先来来了解一下数据流的概念
什么是数据流?
数据流是一串连续的数据集合,就像水池里的水流,在一端水管供水(即写数据),在另一端水管出水(即读数据)。在写入数据时,可以一点一点或一段一段地写入,这些数据会按照先后顺序形成一个长的数据流,则在读取数据时,可以读取任意长度的数据
Java标准库对流进行了一系列的封装,提供了一系列类来完成这些工作,而这些类大体可以分为两大类:
1. 字节流:以字节为单位进行读写,一次最少读写一个字节(输入:InputStream 输出:OutputStream)
2. 字符流:以字符为单位进行读写,一次最少读写一个字符,例如,若使用UTF- 8,则一个汉字占3个字节,每次读写都得以一个汉字为单位来进行读写,不能一次读写半个字符(输入:Reader 输出:Writer)
字节流
我们首先来看读文件操作
InputStream
InputStream(输入流),程序从输入流读取数据(数据来源:键盘、文件、网络...)
常用方法:
返回值类型 | 方法 | 说明 |
int | read() | 读取一个字节的数据,当返回-1时则表示已经读取完毕 |
int | read(byte[] b) | 最多读取长度为b.length字节的数据到b数组中,返回实际读取到的数量,当返回-1时表示已经读取完毕 |
int | read(byte[] b, int off, int len) | 最多读取 len 字节的数据到b数组中,从off下标开始存放数据,返回实际读取到的数量,当返回-1时代表已经读取完毕 |
void | close() | 关闭字节流 |
InputStream是一个抽象类,要使用需要具体的实现类。InputStream的实现类有很多(基本可以认为不同的输入设备都可以对应一个InputStream类),在这里我们需要从文件中读取,因此使用的实现类为 FileInputStream
FileInputStream
构造方法:
构造方法 | 说明 |
FileInputStream(File file) | 利用File构造文件输入流 |
FileInputStream(String name) | 利用文件路径来构造文件输入流 |
在读取文件内容时,步骤为:打开文件 读文件 关闭文件
由于我们还没有学习写文件操作,因此我们手动向文件中写入数据,再从文件中读取数据
我们可以通过刚才学习的createNewFile()方法在当前路径下创建一个新文件,再写入数据
//在当前路径下创建文件 File file = new File("./test.txt"); file.createNewFile(); //手动向文件中写入数据:abcdefg
我们首先一个字节一个字节地读取数据:
package IO; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Demo7 { public static void main(String[] args) throws IOException { //在当前路径下创建文件 File file = new File("./test.txt"); file.createNewFile(); //手动向文件中写入数据:abcdefg //打开文件 InputStream inputStream = new FileInputStream(file); //读文件 while (true){//循环读取,直到读取完数据 int n = inputStream.read(); if(n == -1){//当返回值为-1时, break; } System.out.println(n);//打印读取到的数据 } //关闭文件 inputStream.close(); } }
运行结果:
由于我们是直接打印int类型的n,因此打印结果是字符的ASCII码值
read()的返回值类型是int,但其实际取值为:-1 - 255,当读取到文件末尾,继续读取就会返回-1,其他情况下取值为0 - 255
然而,在上述代码中还有一个隐藏的问题:由于上述代码是 按照打开文件、读文件、关闭文件的顺序执行的,因此未出现问题。但若在读取文件的过程中抛出异常或是根据逻辑执行 return 语句,此时close()就未执行到,就会造成文件资源泄露
关闭文件必须执行,因此我们可以使用finally,即:
Java的try操作还提供了一个版本:try with resources
try ( InputStream inputStream = new FileInputStream(file))//一旦代码出了try代码块,try就会自动调用inputStream的close()方法
注:只有实现了Closeable接口的类,才能放到try()中
也可以使用 read(byte[] b)方法,一次读取多个字节,并将读取到的数据放入b数组中:
package IO; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Demo7 { public static void main(String[] args) throws IOException { try ( InputStream inputStream = new FileInputStream("./test.txt")){ //读文件 while (true){//循环读取,直到读取完数据 byte[] buffer = new byte[1024]; int n = inputStream.read(buffer); if(n == -1){//读取完毕 break; } for (int i = 0; i < n; i++) { System.out.printf("%c", buffer[i]); } } } } }
运行结果:
默认情况下,read会将数组填满,但当文件剩余长度不足以填满数组时,返回值就会告诉我们实际填充了多少个字节
而 read(byte[] b, int off, int len) 则是填充数组的一部分,从off下标开始填入数据,填充len长度数据,若文件剩余长度小于len,此时返回值也会告诉我们实际填充了多少个字节
运行结果:
对比以上三种读取数据方式,相比于一次读取一个字节的数据,一次读取多个字节的IO次数更少,性能更好。
以字节为单位进行读写,是否能够读取中文?
我们将test.txt中内容换成中文:
运行结果:
注:在写入中文时使用的是UTF-8编码,且在String中使用的编码也为UTF-8,因此可以正确读取数据,若我们将编码方式改为“GBK”,则不能正确读取出数据
String s = new String(buffer, 0, n,"GBK");
因此,我们要确保写入数据和读取数据时的编码方式相同,在IDEA中,可在右下角查看使用的编码方式
在我们未学习InputStream之前,我们读取数据是通过Scanner来进行读取的。在上述例子中,对字符类型直接使用InputStream读取比较麻烦,因此,我们可以使用Scanner来进行读取
Scanner(InputStream inputStream, String charset) //使用charset字符集对inputStream进行扫描读取
package IO; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; public class Demo7 { public static void main(String[] args) throws IOException { try (InputStream inputStream = new FileInputStream("./test.txt"){ Scanner scanner = new Scanner(inputStream); //读文件 while (scanner.hasNext()){ String s = scanner.nextLine(); System.out.println(s); } } } }
在学习了文件读取操作后,我们来看写入数据操作
OutputStream
常用方法:
返回值类型 | 方法 | 说明 |
void | write(int b) | 写入数据b |
void | write(byte[] b) | 将数组b中数据都写入 |
int | write(byte[] b, int off, int len) | 将数组b中数据从off下标开始写入,一共写len个数据 |
void | close() | 关闭字节流 |
void | flush() | 在进行写入数据时,为了减少设备操作的次数,会将数据先暂时写入内存中一个指定区域里,直到该区域满了或满足其他指定条件时才会将数据写到设备中,这个区域一般称为缓冲区。但我们写入的数据可能会遗留一部分在缓存区中,需要在最后或合适的位置,调用flush()(刷新)操作,将数据刷到设备中 |
OutputStream同样是一个抽象类,要使用需要具体的实现类。由于我们只关注将数据写入文件,因此使用FileOutputSteam
FileOutputStream
构造方法:
构造方法 | 说明 |
FileOutputStream(File file) | 利用 File 构造文件输出流 |
FileOutputStream(String name) | 利用文件路径构造文件输出流 |
FileOutputStream的使用与FileInputStream的使用类似
package IO; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; public class Demo8 { public static void main(String[] args) { try(OutputStream outputStream = new FileOutputStream("./test.txt")){ //一次写入一个字节数据 outputStream.write(97); outputStream.write(98); outputStream.write(99); //将数组中所有数据都写入 byte[] bytes = {100, 101, 102, 103, 104}; outputStream.write(bytes); //写入数组中部分数据 outputStream.write(bytes,1, 2); //将缓存区数据写到文件中 outputStream.flush(); }catch (IOException e){ e.printStackTrace(); } } }
运行结果:在test.txt中,我们发现数据已经被写入
但上一次手动输入的“你好”被覆盖成新的数据,多运行几次代码,我们会发现:每次写入的数据都会覆盖掉旧的数据。OutputStream默认情况下,会将文件之前内容清空,然后重新写入数据(清空是打开操作引起的,而不是write)。若我们想接着上次的内容继续写,可以使用续写,在打开文件时打开续写开关,即可实现续写
OutputStream outputStream = new FileOutputStream("./test.txt", true)
以字节为单位,是否能够写入中文?
package IO; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; public class Demo8 { public static void main(String[] args) { try(OutputStream outputStream = new FileOutputStream("./test.txt", true)){ String s = "你好"; byte[] b = s.getBytes("UTF-8"); outputStream.write(b); //将缓存区数据写到文件中 outputStream.flush(); }catch (IOException e){ e.printStackTrace(); } } }
字符流
在学习完字节流的相关操作后,此时再学习字符流操作就很简单了
字符流是一个字符一个字符的读,因此只能用来操作文本(不能写图片、音频、视频等)
Reader
常用方法:
返回值类型 | 方法 | 说明 |
int | read() | 读取一个字符,当读取完毕,返回-1 |
int | read(char[] cbuf) | 读取cbuf.length长度的字符到cbuf中,返回实际读取到的数量,当读取完毕,返回-1 |
int | read(char[] cbuf, int off, int len) | 最多读取len大小的字符数据到cbuf中,从off下标开始存放,返回实际读取到的数量,当读取完毕,返回-1 |
int | read(CharBuffer target) | 将字符读入到字符缓存区target中,返回实际添加到缓冲区的字符数,当读取完毕,返回-1 |
void | close() | 关闭字符流 |
Rreader同样是一个抽象类,要使用需要具体的实现类。与文件中读取相关的类是 FileReader
FilerReader
构造方法:
package IO; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; public class Demo9 { public static void main(String[] args) { try (Reader reader = new FileReader("./test.txt")){ while (true){ char[] buffer = new char[1024]; int n = reader.read(buffer); if(n == -1){ break; } String s = new String(buffer, 0, n); System.out.println(s); } }catch (IOException e){ e.printStackTrace(); } } }
Writer
常用方法:
Writer与文件相关具体实现类:FileWriter
FileWriter
构造方法:
示例:
package IO; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; public class Demo10 { public static void main(String[] args) { try(Writer writer = new FileWriter("./test.txt", true)){ String s = "你好"; writer.write(s); }catch (IOException e){ e.printStackTrace(); } } }
练习
在学习了文件基础知识和文件相关操作后,我们通过一些练习来进一步加深我们对知识的理解
练习1
扫描指定目录,找到名称中包含指定字符的所有普通文件
要在指定目录中查找包含指定字符的普通文件,首先我们要通过键盘输入指定字符和搜索目录,然后在指定目录中查找
代码实现:
package IO; import java.io.File; import java.util.Scanner; public class Demo11 { public static void main(String[] args) { //输入指定信息 Scanner scanner = new Scanner(System.in); System.out.println("请输入要搜索的文件名关键字:"); String name = scanner.next(); System.out.println("请输入要搜索的目录:"); String path = scanner.next(); //判断输入的路径是否存在 File pahtFile = new File(path); if(!pahtFile.isDirectory()){ System.out.println("搜索目录有误!"); return; } //在指定路径下查找目标文件 //通过递归的方式来搜索目标文件 scanDir(pahtFile,name); } private static void scanDir(File file, String filename){ //列出当前目录下所有文件 File[] files = file.listFiles(); //若目录为空,直接返回 if(files == null){ return; } //若不为空,遍历目录下所有文件 for (File f: files) { if(f.isFile()){//遍历到普通文件,判断是否为目标文件 if(f.getName().contains(filename)){ System.out.println("符合目标文件:" + f.getName()); } }else if(f.isDirectory()){//遍历到目录文件,继续递归 scanDir(f, filename); } } } }
练习2
普通文件的复制
要实现普通文件的复制,我们首先要获取复制的源文件路径和复制目标文件路径,并判断路径是否正确,然后再从源文件中读取数据并写入到目标文件中
代码实现:
package IO; import java.io.*; import java.util.Scanner; public class Demo12 { public static void main(String[] args) { //输入指定信息 Scanner scanner = new Scanner(System.in); System.out.println("请输入要复制的源文件:"); String src = scanner.next(); System.out.println("请输入要复制的目标文件"); String dest = scanner.next(); //判断输入信息是否符合要求 //源文件必须存在且是普通文件 File srcFile = new File(src); if(!srcFile.isFile()){ System.out.println("源文件有误!"); return; } //目标文件可以存在也可以不存在,但其目录必须存在 File destFile = new File(dest); if(!destFile.getParentFile().isDirectory()){ System.out.println("目标路径有误!"); return; } try (InputStream inputStream = new FileInputStream(srcFile); OutputStream outputStream = new FileOutputStream(destFile)){ while (true){ byte[] buffer = new byte[1024]; //读取源文件中数据 int n = inputStream.read(buffer); if(n == -1){//读取完毕 break; } //将数据写入到目标文件中 outputStream.write(buffer,0,n); } }catch (IOException e){ e.printStackTrace(); } } }