1.文件系统的背景知识
文件的概念
我们先来认识狭义上的文件(file)。针对硬盘这种持久化存储的I/O设备,当我们想要进行数据保存时,
往往不是保存成一个整体,而是独立成一个个的单位进行保存,这个独立的单位就被抽象成文件的概
念,就类似办公桌上的一份份真实的文件一般。
文件目录
同时,随着文件越来越多,对文件的系统管理也被提上了日程,如何进行文件的组织呢,一种合乎自然
的想法出现了,就是按照层级结构进行组织 —— 也就是我们数据结构中学习过的树形结构。这样,一
种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹(folder)或者目录(directory)的
概念
文件路径(path)
如何在文件系统中如何定位我们的一个唯一的文件就成为当前要解决的问题,但这难不倒计算机科学
家,因为从树型结构的角度来看,树中的每个结点都可以被一条从根开始,一直到达的结点的路径所描
述,而这种描述方式就被称为文件的绝对路径(absolute path)。
除了可以从根开始进行路径的描述,我们可以从任意结点出发,进行路径的描述,而这种描述方式就被
称为相对路径(relative path),相对于当前所在结点的一条路径。
比如说我想定位到D盘A文件下的B文件下的C文件下的111文件。有以下这几种常见的路径
D:/A/B/C/111 这是绝对路径。 如果我的工作目录是D:/,那么相对路径就为:./A/B/C/111。
如果我的工作目录是D:/A,那么相对路径就为:./B/C/111。
如果我的工作目录是D:/A/B/C/222,那么相关路径表示为…/111.
如果我的工作目录是D:/A/B/C/222/aaa,那么相关路径表示为…/…/111.
注:
IDEA的工作路径默认就是我当前的项目所在,如果代码里写了一些相对路径,工作路径就以这个去切换。
文件类型
文件的类型大体可以分为两类,一类是文本文件(存的文本,字符串),一类是二进制文件(存的是二进制,不一定是字符串)。也就是说文本文件一定是合法字符,一定是在指定的字符编码的集合内
所以一个文件是不是文本文件,就用记事本打开,如果打开是乱码,那就说明不是文本文件
2.java文件系统
2.1 File类
java标准库里提供了一个File类
File类中的属性
构造方法
在new File对象的时候,构造方法参数中,可以指定一个路径,此时对File对象就代表这个路径对应的文件了,这个路径可以是相对的,也可以是绝对的。
2.2针对文件系统的操作
示例:
package io;
import java.io.File;
import java.io.IOException;
public class IODemo1 {
public static void main(String[] args) throws IOException {
File file1 = new File("d:/test1.txt");//不要求这个路径里真的存在一个test。txt
//上述路径中是一个绝对路径
System.out.println(file1.getName());
System.out.println(file1.getParent());
System.out.println(file1.getPath());
File file2 = new File("./test2.txt");//相对路径去new
System.out.println(file2.getName());//test2.txt
System.out.println(file2.getParent());//其父节点是.
System.out.println(file2.getPath());//new的时候写的什么路径,这就是什么路径,可能是相对路径,可能是绝对路径
System.out.println(file2.getAbsoluteFile());//绝对路径,但是会多一个.\
System.out.println(file2.getCanonicalFile());//绝对路径
}
}
2.3java文件的操作
针对文件内容,我们使用“流对象”进行操作的。
先来说说什么是流对象,所谓流对象是核心就在于这个流字,举个例子,通过一个水龙头去接水,我们接100ml水,可以一次接完,也可以一次接50ml,分两次接;还可以接10mln分10次接。即在读文件的100字节,可以一次读100字节,一次读完,可以一次读50字节,两次读完,可以一次读10字节,十次读完。所谓流就是读取一个文件,可以连续不断、随心所欲地去读取,这个是操作系统的api就是这样设定的
java标准库的流对象可以分为两个大类:字节流和字符流
常见的字节流类(主要用于二进制数据)主要用于操作InputStream 、FileInputStream、 OutputStream、 FileOutputStream
常见的字符流类是(主要用于操作文本数据):Reader、 FileReader、 Writer FileWriter
这些类的使用方法是固定的,核心就是四个
1)打开文件(构造对象)
2)关闭文件(close)
3)读文件(read) =>针对InputStream/Reader
4)写文件(write)=>针对OutputStream/Writer
2.3.1 InputStream与OutputStream
InputStream
:输入流,OutputStream
:输出流, InputStream
、OutputStream
都是抽象类。FileInputStream
FileOutputStream
继承InputStream
OutputStream
所以往往构建一个输入输出流对象会向下面的写法
OutputStream outputStream = new FileOutputStream("文件位置")
2.3.1.1read操作
示例:
package io;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class IODemo2 {
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("D:/test.txt");
//FileInputStream需要抛出异常FileNotFoundException,应对文件没有找到时抛出
//进行读操作
while(true){
int b = inputStream.read();//read 需要抛出IOException,因为FileNotFoundException是IOException的子类,所以直接主函数里抛出IOException
System.out.println("返回值二进制" + Integer.toBinaryString(b));
System.out.println("返回值十进制" + b);
if(b == -1){
break;
}
}
inputStream.close();//一定不要忘记close
}
}
在D盘下里创建一个txt文档,里面写入hello
执行结果
package io;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class IODemo2 {
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("D:/test.txt");
//FileInputStream需要抛出异常FileNotFoundException,应对文件没有找到时抛出
//进行读操作
while(true){
int b = inputStream.read();//read 需要抛出IOException,因为FileNotFoundException是IOException的子类,所以直接主函数里抛出IOException
System.out.println("返回值二进制" + Integer.toBinaryString(b));
System.out.println("返回值十进制" + b);
if(b == -1){
break;
}
}
inputStream.close();
}
}
注意:
- 这些数字就是h e l l o 的ASCII码
- 还有一点说明一下,比如说read读取的是一个字节,返回的是这个字节对应的值(如果是英文字符,那么返回的正好就是字符的ascii值),但是我们注意到实际返回的不是byte类型而是int 类型,所以这个方法的返回值要注意是用int接收。
我们同样来看一个汉字的情况,我们将test.txt 改为文本文件,输入“”你好“”
package io;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class IODemo2 {
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("D:/test.txt");
//FileInputStream需要抛出异常FileNotFoundException,应对文件没有找到时抛出
//进行读操作
while(true){
int b = inputStream.read();//read 需要抛出IOException,因为FileNotFoundException是IOException的子类,所以直接主函数里抛出IOException
System.out.println("返回值二进制" + Integer.toBinaryString(b));
System.out.printf("返回值十六进制%x\n" , b);
System.out.println("返回值十进制"+b);
if(b == -1){
break;
}
}
inputStream.close();
}
}
所以这里就展示了用字节流去读文本文件可不可以读呢?实际也是可以的,上面就是例子,但是不方便,读出来的一个一个字节,不方便。
我们再来看一下read的第二个版本read(Byte[] b)
这个方法需要提前构造一个数组,用于接收read读出来的数据
先看示例
package io;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class IODemo3 {
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("d:/test.txt");
byte[] buffer = new byte[1024];
int len = 0;
while(true){
len = inputStream.read(buffer);
System.out.println("len" + len);
if(len == -1){
break;
}
for (int i = 0; i < len; i++) {
System.out.printf("%x\n",buffer[i]);
}
}
inputStream.close();
}
}
这里需要对这个代码有所解释,否则有些难以理解,首先这里看似有while循环,这里实际只循环了两次。
len = inputStream.read(buffer);
- 这里的传参操作,相当于是把刚才准备好的数组,交给read方法,让read方法内部针对这个数组进行填写,(此处的参数相当于是输出型参数,就很像c里面加了& 符号的形参)。也就说,在第一个示例中read参数为空的时候,我们的read函数本身返回值就是每次读出的一个字节(二进制形式),并且以int的形式返回。所以一个输入流需要读多次,每次返回一个字节的二进制内容。每次返回的是int型。
- 而当read方法有一个byte类型的数组作为参数传入时,read就将每次读到的结果存入这个byte数组中,并且一次就读完这个输入流,将结果存入数组中。此时read本身的返回值是6,也即是输入流里的字节数,也就是说第一次循环后,len = 6,buffer数组里面存的是e4 bd a0 e5 a5 bd(十六进制),然后执行for循环,打印buffer数组,然后在执行第二次循环,此时输入流已经读完了,所以read本身的返回值为-1,直接执行break,退出循环。
- 另外,read的带参数版本,会尽可能的读取字符把buffer 数组排满,比如说本例里buffer数组有1024个元素,只要文本够长就可以提取1024个字节的数据。
根据上面的例子
我们不妨拿一个大一点的文件来看一下。这里我用一个一段《背影》节选作为输入流
)
将for循环屏蔽,打印结果是
证明read了两次,一次读取了1024个字节,一次读取881个字节。
文件在磁盘里,而磁盘比较大,内存比较小,文件很可能不会一次性读入,所以读文件一定是一边读一边去处理数据。
2.3.1.2 write操作
package io;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class IODemo4 {
public static void main(String[] args) throws IOException {
OutputStream outputStream = new FileOutputStream("D:/test.txt");
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
outputStream.write(101);
outputStream.close();
}
}
-
打开OutputStream来说,默认情况下,打开一个文件,就会先清空原有的内容。比如上面例子里面原本的test里面是一段文本的,现在只有abcd四个字母了。
-
这里的Output和Input是相对CPU而言的
-
如果不想清空,流对象还提供一个写追加对象,通过这个就可以实现不清空文件,把新内容追加到后面
-
这里的OutputStream.close操作的含义是关闭文件。那么底层关闭文件究竟做了些什么呢?首先操作系统会维护一张表叫做文件描述符表,这个表在系统里只有一张,每个线程都会打开各种文件,这些文件只要打开就会在文件描述表了产生一个表项来存储这个文件描述符,这个文件描述符实际就是一个file_struct对象。每次关闭文件实际就是将关于这个文件的文件描述符(表中一项)删去。那么问题来了,如果没删呢?会存在这种情况,这个表的表项会越积越多,文件描述符表是没有办法扩容的,会导致这个表满的情况。虽然java有GC机制可以会在回收outputStream这个对象的时候释放这个表里的内存,但是这并不是及时的。
所以在涉及输入输出流的代码里OutputStream.close(),InputStream.close()是一定要写的。所以下面这种写法是跟推荐的(更优雅)
这个语法在java中称之为try with resources;这个方法虽然没有显示的写close,实际上是会执行的,只要try语句块执行完毕,就可以自动执行到close!
package io;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class IODemo4 {
public static void main(String[] args) throws IOException {
try (OutputStream outputStream = new FileOutputStream("D:/test.txt")) {
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
outputStream.write(101);
}
}
}
注意
- 并不是说所有的对象都可以放入try()里面就能自动释放,必须满足一定要求,这些对象必须是实现closeable接口的类,才可以放大try的括号中去,这个方法就是提供了close()方法。
2.3.2Reader 和 Writer
提前在D盘下面创建test.txt文件,并写入abcd
然后执行下面代码
package io;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class IODemo5 {
//字符流的操作
public static void main(String[] args) {
try(Reader reader = new FileReader("D:/test.txt")){
while(true){
int ch = reader.read();
if(ch == -1){
break;
}
System.out.println(""+(char)ch);
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
结果
Writer append(char c)
将指定的字符附加到此文件。
Writer append(CharSequence csq)
将指定的字符序列附加到此文件。
Writer append(CharSequence csq, int start, int end)
将指定字符序列的子序列附加到此文件。
abstract void close()
关闭流,先刷新。
abstract void flush()
刷新流。
void write(char[] cbuf)
写入一个字符数组。
abstract void write(char[] cbuf, int off, int len)
写入字符数组的一部分。
void write(int c)
写一个字符
void write(String str)
写一个字符串
void write(String str, int off, int len)
写一个字符串的一部分。
示例
package io;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class IODemo6 {
public static void main(String[] args) {
try(Writer writer = new FileWriter("d:/test.txt")){
writer.write('a');
writer.write("bcd");
writer.write("efdh",2,1);
writer.append('a');
writer.append("abcd");
writer.append("bbbb",1,3);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.4 缓冲区机制
其实还有一点要注意,write,read等操作,其实真的都是在具有缓冲区机制的,write这样的写操作,是先写到缓冲区(缓冲区存在很多形态,咱们代码里缓冲区,标准库里有缓冲区,操作系统内核里有缓冲区…)等到缓冲区满了才会从缓冲区写到设备里或者磁盘上。
close()操作还需要拎出来在单独说一说,每一次close的执行都会触发缓冲区的冲刷操作(flush)有时候也称之为刷新操作,就是把缓冲区里的数据都写入硬盘里
2.5Scanner结合字符流对象来使用
我们常见的编程中Scanner 常搭配System.in使用,
Scanner scanner = new Scanner(System.in);
这个System.in实际就是一个输入字节流
所以Scnanner里面的参数也可以换成一个字节流,所以我们可以利用Scanner从一个文本文件里面读取数据
比如说,我们在D盘下创建一个test.txt的文本文件里面写入一些数据
示例
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class IODemo7 {
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("d:/test.txt")) {
Scanner scanner = new Scanner(inputStream);
while(scanner.hasNext()){
String str = scanner.next();
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意
- 这里Scanner本身确实也是有close()的,但是Scanner没有放在try()里面,这是因为Scanner内部的InputStream已经close了,所以Scanner不关闭也没事。
3.关于文件系统的小案例
3.1案例1:普通文件的删除
给定一个目录,目录里包含了很多的文件和子目录,用户输入一个要查询的词,看看当前目录下(以及子目录里)是否有匹配的结果,如果有匹配的结果,就进行删除。
示例
package io;
import java.io.File;
import java.util.Scanner;
public class IODemo8 {
public static void main(String[] args) {
//让用户输入一个指定搜索的目录
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的路径:");
String basePath = scanner.next();
//针对用户输入进行简单的判定
File root = new File(basePath);
if(!root.isDirectory()){
System.out.println("您要搜索的目录不正确!");
return;
}
System.out.println("请输入你要删除的文件名");
String nameToDelete = scanner.next();
scanDir(root,nameToDelete);
}
private static void scanDir(File root,String nameToDelete){
//指定路径进行扫描,递归操作
//先从根目录出发(root)
//先判定一些,当前的这个目录里,看看是否包含咱们要删除的文件,如果有救删除,如果没有就跳过,下一个
//1. 先列出root下的文件和目录
File[] files = root.listFiles();//listFiles方法的目的是读取当前对象(root)下的文件和目录
Scanner scanner = new Scanner(System.in);
if(files == null){
return ;
}
for (File f:files) {
{//首先判断是不是目录
if(f.isDirectory()){
scanDir(f, nameToDelete);
}
else{
//如果是普通文件,就判定是否要删除。
if(f.getName().contains(nameToDelete)){//注意是输入的文件名只要包含在f的name里面就像,因为getnaem获取的还有文件的类型
System.out.println("是否要删除"+f.getPath());
String input = scanner.next();
if(input.equals("是")){
f.delete();
System.out.println("删除成功!");
}else{
System.out.println("删除取消!");
}
}
}
}
}
}
}
我们先在D下创建tmp、A、B文件夹,test.txt文件,以这个实例来验证
3.2 案例2:进行普通文件的复制
package io;
import java.io.*;
import java.util.Scanner;
public class IODemo9 {
static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
System.out.println("请输入您源文件所在的路径:");
String srcpath = scanner.next();
System.out.println("请输入您目标文件所在路径:");
String despath = scanner.next();
copy(srcpath,despath);
}
public static void copy(String srcpath,String despath){
File file1 = new File(srcpath);
File file2 = new File(despath);
if(file1.isDirectory()){
//如果源文件是一个目录或者不存在
System.out.println("您输入的源文件有误!");
return;
}
if(file2.isFile()){
//如果目标文件已经存在
System.out.println("目标文件已经存在");
return;
}
try(InputStream inputStream = new FileInputStream(file1); OutputStream outputStream = new FileOutputStream(file2)) {
//try()括号里面可以同时new多个实现了close的对象
while(true){
int tmp = inputStream.read();
if(tmp == -1){
break;
}
outputStream.write(tmp);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们在D盘下面创建一个resource.txt文件,然后将其复制到D:/tmp下并命名为destinnation.txt文件
创建成功!
有几个细节需要说明一下:
- 这里的OutputStream在写文件的时候,文件不存在就会自动创建,但是InputStream不行,文件不存在,就抛异常了