1. 什么是IO
IO就是单词Input和Output的缩写.Input是输入,Output是输出.
我们怎么确定是一个操作是输入还是输出呢?这其实是一套人为规定的规则.
网卡->硬盘->内存->CPU
如果数据符合上述的传输方向,就是输入;反之,就是输出.比如,我们在控制台上输入一个数,这个数从硬盘存储到内存中,就是输入;从内存中读取数据到硬盘中,就是输出.这套规则还是比较符合我们的常识与直觉的.
2. 文件
2.1 文件的概念
我们提到的"文件"是一个十分狭义的概念,指的就是保存在硬盘上的文件.
文件夹也是一种文件,称为"目录文件",也保存在硬盘上.在一个硬盘中,可能有许多目录文件,其中又嵌套着目录文件.把这个数据结构抽象出来,得到的就是一个N叉树型结构.
2.2 文件的路径
既然文件这么多,我们需要一种属性来标识出这个文件存放在哪里,这就是文件的路径.文件的路径获取方式:从根节点出发,一层一层往下走,最终达到目标文件之后,中间的这些目录集合在一起,就构成了路径.
2.2.1 绝对路径
以盘符为开头的就是绝对路径.
路径形式:"C:/program/qq/Bin/qq.exe".注:推荐使用'/',在Window系统下,'/'和'\'都支持;但是在其他系统下可能只支持'/'.
2.2.2 相对路径
比如我们现在仍然需要找qq.exe
如果我们当前的基准目录是
C:/program/qq/Bin,通过./qq.exe可以找到这个文件.
C:/program/qq,通过./Bin/qq.exe可以找到这个文件.
如果我们现在基准目录是
C:/program/qq/Bin/plugins,通过../qq.exe可以找到这个文件.
总结:
当前基准文件(用.代替)+后续路径 = 该文件的绝对路径.
使用..来返沪上一层目录.
2.3 文件的分类方式
文件的分类方式又很多种,这里主要讨论一种和编写代码密切相关的:文本文件和二进制文件.
2.3.1 文本文件
文本文件在硬盘上存储的数据是文本数据,这些文件的数据中都是字符串(本质上是能够构成合法字符的二进制数据),每一个部分都是合法的字符.(通过GBK,UTF8等码表让数字和字符一一对应).这是给人看的文件.
2.3.2 二进制文件
二进制文件与文本文件相对,其中保存的就是纯二进制数据,这些二进制数据通过码表对应过来可能是一堆乱码.这是给机器看的文件.
区分清楚一个文件到底是文本文件还是二进制文件是十分重要的.因为对于两种文件的编程处理方式是不同的.
那么到底怎么区分呢?最简单的方式就是使用记事本打开这个文件,读得懂就是文本文件,出现一堆乱码就是二进制文件.
对于日常常见的一些文件:
docx,pptx,mp3,mp4,pdf 都属于二进制文件;md,html,java,c 都属于文本文件.
3. 使用Java操作文件
3.1 针对文件系统进行操作
3.1.1 获取文件的路径
针对文件系统的操作包括:创建文件,删除文件,创建目录,重命名文件......
Java标准库提供了一个File类表示一个文件,进一步通过File提供的方法,就可以进行文件系统的操作了.
假如我们在"C:\code\test\io",这个路径下创建了一个文本文件:test.txt
public static void main(String[] args) throws IOException {
File f = new File("./test.txt");
System.out.println(f.getParent());//获取上一级目录
System.out.println(f.getName());//获取文件名称
System.out.println(f.getPath());//获取当前工作路径
System.out.println(f.getAbsolutePath());//获取拼接当前工作路径和相对路径
System.out.println(f.getCanonicalPath());//获取简化后的拼接路径
}
结果如下:
3.1.2 文件的创建
public static void main(String[] args) throws IOException {
File f = new File("./test.txt");
f.createNewFile();
System.out.println(f.exists());//是否存在
System.out.println(f.isFile());//是否是文件
System.out.println(f.isDirectory());//是否是目录
}
结果如下:
文件的创建并不一定总是会成功的,当权限不足或者给出的路径非法时,文件的创建就会失败.
创建成功后,左侧列表就会出现一个文本文件.
3.1.3 文件的删除
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 文件删除
File f = new File("./test.txt");
// f.delete();
f.deleteOnExit();
// 把程序阻塞住
scanner.next();
System.out.println(f.exists());
}
当我们这个程序运行完之后,左侧的文本文件就消失了.结果如下图所示:
3.1.4 目录的创建
我们也可以通过File类中的一些方法来创建目录.
public static void main(String[] args) {
File dir = new File("./testDir/aaa/bbb/ccc/ddd");
// mkdir 只能创建出一级目录
// dir.mkdir();
// 可以创建多级目录
dir.mkdirs();
System.out.println(dir.isDirectory());
}
同样会在左侧的列表中出现一个我们创建的目录,结果如下:
3.1.5 更改文件名称
public static void main(String[] args) {
File f1 = new File("./test2.txt");
File f2 = new File("./testDir/test2.txt");
f1.renameTo(f2);
}
这里的意思相当于将文件f1重新命名为f2.
3.2 Java对于文件内容进行操作
3.2.1 Stream流
对于文件内容的操作有:读文件,写文件,打开文件和关闭文件.
在Java中通过"流"(stream流)这样的一组类,进行上述的文件内容操作.
这个"流"分成两组:字节流和字符流.
字节流以字节为单位读取数据.主要针对二进制文件读写数据.主要涉及的两个流对象是InputStream和OutputStream.
字符流以字符为读取单位.主要针对文本文件读写数据.主要涉及的两个流对象是Reader和Writer.
注意: 字符!=字节,一个字符往往对应了多个字节.
3.2.2 核心操作
1. 通过构造方法,打开文件.
2. 通过read方法读文件内容.
3. 通过write方法写文件内容.
4. 通过close关闭文件.
3.2.3 字节流的使用
3.2.3.1 读文件
首先,我们需要创建一个流对象inputStream,但是我们不能直接new出一个InputStream的实例.
因为InputStream是一个抽象类,需要一个类来继承它,这个类我们也有现成的,就是FileInputStream
那么我们new一个FileInputStream的实例,再使用InputStream向上转型接收即可.
然后,我们需要创建一个大小为1024,byte类型的数组来存放读取到的字节.
随后,我们就要开始读取了,调用inputStream对象的read方法.
3.2.3.2 两个read方法API的比较
1.read(),一个字节一个字节读,频次高
2.read(byte[]),一次读许多个字节,频次低
这两个方法哪一个执行的效率高?答:第二个,因为它访问IO设备的次数少,这个过程是十分耗费时间的.在使用Java做算法题时,有的题目就会卡输入输出,输入输出超时的原因就是访问IO设备的次数过多,我们解决的方法是减少IO访问次数,欲知后事如何,请等待下篇博客.
我们这里直接调用参数只有一个byte类型数组的read方法即可.这个方法标识的返回值是从-1到整型的最大值,但实际读取到字节的范围是0-255.返回值设置为int的原因是,当返回-1时,就可以标识着读取结束.
接着,我们可以打印出来看一下,使用for循环+printf使用16进制格式化输出.
最后,需要调用inputStream对象的close关闭流.
3.2.3.3 close为什么重要
我们在大多数时候没关闭文件是很难感知出来的,但是一旦遇到问题就是大事情.
当我们打开文件时,会在操作系统内核PCB中给"文件描述符表"添加一个元素,这个元素就是表示当前打开的文件相关信息.
close方法的执行相当于就释放了文件描述符表上对应的元素.如果一直打开文件而不关闭,文件描述符表就可能被占满,被占满后,尝试再次打开文件,就会使打开文件失败,可能会使得程序卡死.
总结而言,close释放了文件打开需要的资源,保证文件的打开能够正常进行,十分重要.
接下来,我们编写代码来实现读文件操作.
比如,我现在在"C:/code/test/io/test.txt"这个文件中写了一个"helloworld",我们来写一下代码看看效果:
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("C:/code/test/io/test.txt");
try {
while (true) {
byte[] bytes = new byte[1024];
int n = inputStream.read(bytes);
if(n==-1) {
return;
}
for (int i = 0; i < n; i++) {
System.out.printf("0x%x ",bytes[i]);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
inputStream.close();
}
}
结果如下:
我们来找一下,helloworld里面的字符对应的十六进制数表示形式.
发现与我们的结果相同,最后两个十六进制数其实对应的不是字母,而是换行/回车符,标志字符串在文件中的结尾位置.
虽然这个效果我们达到了,但是这个代码还可以写得更优雅.我们可以把流对象的创建放在try后面的括号里面,这样我们就不需要手动地进行close操作.代码如下:
try(InputStream inputStream = new FileInputStream("C:/code/test/io/test.txt")) {
while (true) {
byte[] bytes = new byte[1024];
int n = inputStream.read(bytes);
if(n==-1) {
return;
}
for (int i = 0; i < n; i++) {
System.out.printf("0x%x ",bytes[i]);
}
}
}
但是并不是任何对象的创建都可以这样操作,要实例化对象的这个类必须是实现了Closeable接口才可以这样进行操作.我们接下来的代码将使用这种方式编写.
3.2.3.4 写文件
假如我们要对上述路径的文件写入一个abcd,我们可以先创建流对象outputStream(同样是向上转型创建),再调用这个流对象的write方法,给它传入一个int型的参数(对应字母的assic码).
代码如下:
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("C:/code/test/io/test.txt")) {
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
}catch (IOException e) {
throw new RuntimeException(e);
}
}
结果如下:
注意:
1. 如果我们在进行写文件时,文件还没有被创建,系统就会自动地为我们创建一个新文件.但是,读操作并不会自动地创建新的文件,如果文件找不到就会抛出FileNotFoundException.
2. 对于写操作,如果这个文件中原先有数据,这些数据会在被打开时清空(被打开操作清空的).但是,我们可以在创建流对象时,多传入一个参数true来实现"追加写".如下图所示:
3.2.4 字符流的使用
3.2.4.1 读文件
这里我们的操作和之前的读操作差不多,不过多赘述.主要的差别在于我们这里直接一个个字符读取,再转换成int型(对于assic码),最后再转回字符输出即可.代码和结果如下:
public static void main(String[] args) throws IOException {
try (Reader reader = new FileReader("C:/code/test/io/test.txt")) {
while (true) {
int c = reader.read();
if(c==-1) {
break;
}
char cc = (char)c;
System.out.print(cc);
}
}catch (IOException e) {
e.printStackTrace();
}
}
3.2.4.2 写文件
这里我们的操作和之前的写操作差不多,直接上代码和结果.代码和结果如下:
public static void main(String[] args) throws IOException {
try (Writer writer = new FileWriter("C:/code/test/io/test.txt",true)) {
writer.write(97);
writer.write(98);
writer.write(99);
writer.write(100);
}catch (IOException e) {
e.printStackTrace();
}
}
3.3 代码案例
3.3.1 案例1
要求:扫描指定目录,找到名称中包含指定字符的所有普通文件,并询问用户是否要删除该文件.代码如下:
public class Demo {
private static void searchFile(File file,String searchWord) {
Scanner scanner = new Scanner(System.in);
File[] files = file.listFiles();
if(files==null) {
return;
}
for(File f :files) {
if(f.isFile()) {
String fileName = f.getName();
if(fileName.contains(searchWord)) {
System.out.println("找到了目标文件:"+f.getAbsolutePath());
System.out.println("是否要删除该文件,删除1/不删除0");
int flag = scanner.nextInt();
if(flag==1) {
f.delete();
return;
}
}
}else if(f.isDirectory()) {
searchFile(f,searchWord);
}
}
}
public static void main(String[] args) {
//1.输入目录路径信息和搜索关键词
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的目录路径:");
String rootPath = scanner.next();
System.out.println("请输入要搜索的关键词:");
String searchWord = scanner.next();
File file = new File(rootPath);
//2.路径合法性判断
if(!file.isDirectory()) {
System.out.println("输入的路径非法!");
return;
}
//3.开始查找
searchFile(file,searchWord);
}
}
这里我们主要分析一下searchFile方法,这个方法内部:
先把当前file中的文件列表放入一个File数组中,如果这个数组为空直接返回,再去遍历访问这个数组中的每一个文件.
如果这个文件是普通文件,判断名称中是否包含关键词.包含就输出,再判断是否要删除.
如果这个文件是目录文件,递归调用searchFile方法,这是一种DFS搜索方式.
3.3.2 案例2
要求:复制一个文件,输入一个路径表示要被复制的文件,输入另一个路径表示要复制到的目标目录.
public static void main(String[] args) {
//1.输入两个路径
Scanner scanner = new Scanner(System.in);
System.out.println("输入被复制文件的路径:");
String srcFilePath = scanner.next();
File srcFile = new File(srcFilePath);
System.out.println("输入需要复制到的文件的路径:");
String desFilePath = scanner.next();
File desFile = new File(desFilePath);
//2.路径合法性判断
if(!srcFile.isFile()) {
System.out.println("输入的路径非法:");
return;
}
if(!destFile.getParentFile().isDirectory()) {
System.out.println("输入的路径非法:");
return;
}
//3.读文件,写文件
try (InputStream inputStream = new FileInputStream(srcFilePath)
;OutputStream outputStream = new FileOutputStream(desFilePath) ) {
while (true) {
byte[] bytes = new byte[1024];
int n = inputStream.read(bytes);
if(n==-1) {
break;
}
outputStream.write(bytes,0,n);
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
对于这个案例我们要注意,我们不是直接去判断des文件是否存在,因为大概率不存在.我们只需要判断des的parentPath是不是目录即可.
3.3.3 案例3
要求:扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录).
private static void searchFile(File file,String searchWord) {
File[] files = file.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isFile()) {
String fileName = f.getName();
if (fileName.contains(searchWord)) {
System.out.println("找到了目标文件:" + f.getAbsolutePath());
return;
}else {
matchWord(f,searchWord);
}
} else if (f.isDirectory()) {
searchFile(f, searchWord);
}
}
}
private static void matchWord(File f, String searchWord) {
try (Reader reader = new FileReader(f)) {
StringBuilder stringBuilder = new StringBuilder();
while (true) {
int c = reader.read();
if(c==-1) {
break;
}
stringBuilder.append((char)c);
}
if(stringBuilder.indexOf(searchWord)>0) {
System.out.println("找到了目标文件:" + f.getAbsolutePath());
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的目录路径:");
String rootPath = scanner.next();
System.out.println("请输入要搜索的关键词:");
String searchWord = scanner.next();
File file = new File(rootPath);
//2.路径合法性判断
if(!file.isDirectory()) {
System.out.println("输入的路径非法!");
return;
}
//3.开始查找
searchFile(file,searchWord);
}
在这个案例中,我们仍然递归查找,只不过是在案例一的基础之上多加了读和匹配操作.这里使用字符流进行读取,利用StringBuilder进行字符串的拼接,最后调用StringBuilder的API判断是否包含关键词.