目录
一、文件
我们平时说的文件一般都是指存储在硬盘上的普通文件。形如 txt, jpg, mp4, rar
等这些文件都可以认为是普通文件,它们都是在硬盘上存储的。
在计算机中,文件可能是一个广义的概念,就不只是包含普通文件,还可以包含目录(把目录称为目录文件)。在操作系统中,还会使用文件来描述一些其他的硬件设备或者软件资源;操作系统中就把网卡这样的硬件设备也给抽象成了一个文件=>简化开发(显示器/键盘操作系统也是把这些设备视为文件)。
本章节所讨论的文件,是针对普通文件来讨论的,后面去学习一些其他的硬件设备对应的文件,其实也是通过类似的代码来实现操作的。
1.1 机械硬盘
机械硬盘的结构:
机械硬盘一旦上电,里面的盘片就会高速运转,磁头就在盘片上找到对应的数据。受限于机械硬盘的硬件结构:盘片转速越高,读写速度就越快,但是因为工艺的限制,盘片的转速也不可能无限高;机械硬盘的读写速度已经有10年停滞未前,机械硬盘都是往大容量的方向发展。这里的这个读写速度就比内存读写慢很多(3-4)数量级。
后面就又有了固态硬盘(SSD),固态硬盘的硬件结构,和机械硬盘截然不同,固态硬盘的读写速度要比机械硬盘高很多,像最好的固态硬盘,读写速度已经接近于十几年前的内存水平了。
1.2 文件的分类
我们一般简单的将文件划分为文本文件和二进制文件,分别指代保存被字符集编码的文本和按照标准格式保存的非被字符集编码过的文件。
- 文本文件:里面存储的是字符,文本文件本质上也是存字节的,但是文本文件中,相邻的字节在一起正好能构成一个个的字符。
- 二进制文件:存储的是字节,这种的话字节和字节之间就完全没啥关系了。
判定一个文件是文本和二进制,一种简单的方法:
用记事本打开,如果打开之后是乱码,就是二进制,不是乱码就是文本。
类似的,像日常中使用的.txt,.c,.java
都属于文本文件..doc, .ppt, .exe, .zip,.class
等等都属于二进制文件.
1.3 文件的目录结构
在计算机里保存管理文件,是通过操作系统中的“文件系统”这样的模块来负责的。
文件系统中,一般是通过"树形"结构来组织磁盘上的目录和文件的。
整体的文件系统,就是这种树形结构.如果是一个普通文件,就是树的叶子节点.
如果是一个目录文件,目录中就可以包含子树,这个目录就是非叶子节点.这个树每个节点上的子树都可以有N个,这就是一个N叉树了.
1.4 文件路径
在操作系统中,就通过"路径"这样的概念,来描述一个具体文件/目录的位置。
路径这里有两种描述风格:
绝对路径
以盘符开头的.
D:\IDEA安装包\install\IntelliJ IDEA 2020.1.3\bin
相对路径
相对路径:以.
或者..
开头的。其中.
表示当前路径, ..
表示当前路径的父目录(上级路径)谈到相对路径,必须要现有一个基准目录。
相对路径就是从基准目录出发,按照一个啥样的路径找到的对应文件。
例如:
以D:\program\jdk\bin
为基准目录,找到 javac.exe
:./javac.exe
此处的.
就表示当前目录(基准目录);
以D:\program\jdk\bin
为基准目录找到src.zip
:../src.zip
,此处的..
就表示基准目录的上一级路径D:\program\jdk
,再从这个D:\program\jdk
路径中去找到src.zip
这个文件。
即使是定位到同一个文件,如果基准目录不同,此时相对路径也不同。
例如:
以D:\program\jdk
路径为基准,去找javac.exe
相对路径./bin/javac.exe
;
以D:\program
路径为基准,去找javac.exe
相对路径.jdk/bin/javac.exe
;
二、Java 中操作文件
Java中操作文件,主要是包含两类操作.
- 文件系统相关的操作[C语言没有,C标准库就不支持这个操作]指的是通过“文件资源管理器"能够完成的一些功能,如:
1)列出目录中有哪些文件
2)创建文件
3)创建目录
4)删除文件
5)重命名文件
Java
中通过 java.io.File
类来对一个文件(包括目录)进行抽象的描述。通过这个类来完成上述操作注意,有 File 对象,并不代表真实存在该文件。
- 文件内容相关的操作
1)打开文件
2)读文件
3)写文件
4)关闭文件
File的构造方法,能够传入一个路径,来指定一个文件.这个路径可以是绝对路径也可以是相对路径。
2.1 File中的常见方法
观察 get 系列的特点和差异
import java.io.File;
import java.io.IOException;
public class Test01 {
public static void main(String[] args) throws IOException {
//绝对路径
File f = new File("g:/test.txt");
System.out.println(f.getParent());//获取到文件的父目录
System.out.println(f.getName());//获取到按文件名
System.out.println(f.getPath());//构造File的时候,指定的路径
System.out.println(f.getAbsolutePath());//获取到绝对路径
System.out.println(f.getCanonicalPath());//获取到绝对路径
System.out.println("=====================");
//相对路径
File f2 = new File("./test.txt");
System.out.println(f2.getParent());//获取到文件的父目录
System.out.println(f2.getName());
System.out.println(f2.getPath());//构造File的时候,指定的路径
//获取到绝对路径在基准路径的基础上,又把相对路径给拼接上来了
System.out.println(f2.getAbsolutePath());
System.out.println(f2.getCanonicalPath());//获取到的是化简过的绝对路径
}
}
输出结果:
绝对路径创建的文件就在指定的路径下面:
通过相对路径创建文件,一定得先明确一个"基准路径",如果是通过IDEA
的方式来运行程序,此时基准路径就是当前java项目所在的路径。
在IDEA中直接运行,基准路径就是上述目录,此处写的"./test.txt
"在IDEA中运行,意思就是找javaIO
目录下的test.txt
。
一旦文件路径指定错误,很容易出现找不到文件的情况。
普通文件的创建、删除
import java.io.File;
import java.io.IOException;
public class Test09 {
public static void main(String[] args) throws IOException {
File file = new File("hello.txt"); 要求该文件不存在,才能看到不一样现象
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
System.out.println("创建文件");
file.createNewFile();//创建文件时抛出异常
System.out.println("创建文件结束");
System.out.println(file.exists());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
System.out.println(file.createNewFile());//已经创建过的文件不能重复创建
}
}
输出结果:
普通文件的删除
package File;
import java.io.File;
import java.io.IOException;
public class Test10 {
public static void main(String[] args) throws IOException {
File file = new File("g:/hello.txt");
System.out.println(file.exists());
file.createNewFile();
System.out.println(file.exists());
file.delete();
System.out.println(file.exists());
}
}
输出结果:
目录的创建
创建目录的时候,使用mkdir
只能创建一级目录,要想一次创建多级,需要使用mkdirs
:
mkdir()
的时候,如果中间目录不存在,则无法创建成功; mkdirs()
可以解决这个问题。
import java.io.File;
public class Test10 {
public static void main(String[] args) {
File file = new File("./aaa/bbb/ccc/ddd");
// file.mkdir();
file.mkdirs();
System.out.println(file.isDirectory());//true
}
}
返回File对象代表的目录下的所有文件名
package File;
import java.io.File;
import java.util.Arrays;
public class Test11 {
public static void main(String[] args) {
File file = new File("./");
//返回File 对象代表的目录下的所有文件名
// System.out.println(Arrays.toString(file.list()));
//返回File对象代表的目录下的所有文件,以File对象表示
System.out.println(Arrays.toString(file.listFiles()));
}
}
输出结果:
文件重命名
import java.io.File;
public class Test11 {
public static void main(String[] args) {
File file1 = new File("./aaa");
File file2 = new File("./sss");
file1.renameTo(file2);
}
}
输出结果:
三、文件内容的读写 —— 数据流
针对文件内容的读写,Java标准库提供了一组类,首先按照文件的内容,分成了两个系列:
1)字节流对象,针对二进制文件,是以字节为单位进行读写的;读:InputStream
,写:OutputStream
,上述都是抽象类,实际使用的往往是这些类的子类:FileInputStream、FileOutputStream.
2)字符流对象,针对文本文件,是以字符为单位进行读写的;读:Reader
,写:Writer
,实际上使用的是这些抽象类的子类:FileReader、FileWriter
.
抽象类既可以针对普通文件的读写,也可以针对特殊文件(网卡,socket文件)进行读写;其子类只能针对普通文件进行读写。
为啥上述内容叫做"流”对象?
流Stream,这是一个形象的比喻。此处我们说的流,就像水流一样,打开开关,就源源不断的感觉。
例如:想通过这个流对象,来读取100个字节.可以一次读10个字节,分10次读完;
也可以一次读20个字节,分5次读完;还可以一次读100个字节,分1次读完。
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 Test12 {
public static void main(String[] args) {
try(InputStream inputStream = new FileInputStream("g:/test.txt")){
while (true){
int len = inputStream.read();
if(len == -1){
break;
}
System.out.println(len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件里面的内容:
输出结果:
在这个代码中,没有显式的调用close
但是try
会帮我们自动调用。
当代码执行完这里的try语句块之后,就会自动的调用close。
得符合一定的条件,才能放到try ( )中.实现Closeable
这个interface
,所有的流对象,都实现了Closeable
,所以就可以直接放了.
读文件–一次读取若干个文件
package File;
import java.io.*;
public class Test02 {
public static void main(String[] args) {
try(InputStream inputStream = new FileInputStream("g:/test.txt")){
while (true){
//一次读取若干个字节
//这个操作是把读出来的结果放到 buffer这个数组中了.
//相当于是使用参数来表示方法的返回值.这种做法称为“输出型参数"
//这种操作在Java中比较少见.C++中遍地都是~~
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);
if(len == -1){
break;
}
for (int i = 0; i < len; i++) {
System.out.println(buffer[i]);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用字节流写文件
package File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class Test03 {
public static void main(String[] args) {
try(OutputStream outputStream = new FileOutputStream("g:/test.txt")){
//一次写入一个字节
// outputStream.write(97);
// outputStream.write(98);
// outputStream.write(99);
//一次写入多个字节
byte[] buffer = new byte[]{97,98,99,100};
outputStream.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用字符来读
package File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class Test04 {
public static void main(String[] args) {
try(Reader reader = new FileReader("g:/test.txt")){
//按照字符来读
while (true){
char[] buffer = new char[1024];
int len = reader.read(buffer);
if(len == -1){
break;
}
// for (int i = 0; i < len; i++) {
// System.out.println(buffer[i]);
// }
String s = new String(buffer,0,len);
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出结果:
按照字符来写
package File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
//按照字符来写
public class Test05 {
public static void main(String[] args) {
try(Writer writer = new FileWriter("g:/test.txt")){
writer.write("xyz");
} catch (IOException e) {
e.printStackTrace();
}
}
}
应用1:实现查找文件并删除
扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录), 并且后续询问用户是否要删除该文件。
package File;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
/**
* 扫描指定目录,并找到名称中包含指定字符的所有普通文件(不包含目录),
* 并且后续询问用户是否要删除该文件
*/
public class Test06 {
private static void scanDir(File rootDir,String toDeleteName){
//列出rootDir中的内容
File[] files = rootDir.listFiles();
if(files == null){
return;
}
for(File f : files){
if(f.isFile()){
if(f.getName().contains(toDeleteName)){
// 不要求名字完全一样, 只要文件名中包含了关键字即可删除
deleteFile(f);
}
}else if(f.isDirectory()){
scanDir(f,toDeleteName);
}
}
}
private static void deleteFile(File f) {
try {
System.out.println(f.getCanonicalPath() + "确定要删除吗?Y/y");
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
if(str.equals("Y") || str.equals("y")){
f.delete();
System.out.println("文件删除成功!");
}else {
System.out.println("文件取消删除!");
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//1.输入要遍历的目录,以及要删除的文件名
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要扫描的路径:");
String rootDirPath = scanner.next();
System.out.println("请输入要删除的文件名:");
String toDeleteName = scanner.next();
File rootDir = new File(rootDirPath);
if(!rootDir.isDirectory()){
System.out.println("输入的路径有误!!!");
return;
}
//2.遍历目录, 把 指定目录 中的所有文件和子目录都遍历一遍, 从而找到要删除的文件
scanDir(rootDir,toDeleteName );
}
}
文件原来存放位置为G盘下 test.txt.
输出结果:
应用2:进行普通文件的复制
import java.io.*;
import java.util.Scanner;
//进行普通文件的复制
public class Test07 {
public static void main(String[] args) {
Scanner sc= new Scanner(System.in);
System.out.println("输入要拷贝的源文件:");
String src = sc.nextLine();
System.out.println("输入要拷贝的目标文件:");
String dest = sc.nextLine();
File file = new File(src);
//此处不太需要检查目标文件是否存在.
//OutputStream 写文件的时候能够自动创建不存在的文件.
if(!file.isFile()){
System.out.println("源文件路径不正确!!!");
return;
}
try(InputStream inputStream = new FileInputStream(src)) {
try (OutputStream outputStream = new FileOutputStream(dest)){
byte[] buffer = new byte[1024];
while (true){
int len = inputStream.read(buffer);
if(len == -1){
break;
}
// 写入的时候, 不能把整个 buffer 都写进去. 毕竟 buffer 可能是只有一部分才是有效数据.
outputStream.write(buffer,0,len);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出结果:
应用3:扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下实验
进行文件内容的查找:先输入一个路径,再输入一个要查找的文件内容的"关键词"
递归的遍历文件,找到看哪个文件里的内容包含了关键词,就把对应的文件路径打印出来。
总结:先递归遍历文件,针对每个文件都打开,并读取内容,再进行字符串查找即可。
package File;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Scanner;
//扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
public class Test08 {
public static void main(String[] args) throws IOException {
//输入要扫描的文件路径
Scanner sc = new Scanner(System.in);
System.out.println("请输入要扫描的路径:");
String rootDirPath = sc.next();
System.out.println("请输入要查找的关键词:");
String word = sc.next();
File rootDir = new File(rootDirPath);
if(!rootDir.isFile()){
System.out.println("输入的路径非法!");
return;
}
scanDir(rootDir,word);
}
private static void scanDir(File rootDir, String word) throws IOException {
//列出这个路径下的所有文件
File[] files = rootDir.listFiles();
if(files == null){
return;
}
for(File f : files){
if(f.isFile()){
if(containsWord(f,word)){
//输出f的绝对路径
System.out.println(f.getCanonicalPath());
}
}else if(f.isDirectory()){
scanDir(f,word);
}
}
}
private static boolean containsWord(File f, String word) {
StringBuilder stringBuilder = new StringBuilder();
// 把 f 中的内容都读出来, 放到一个 StringBuilder 中
try(Reader reader = new FileReader(f)){
char[] buffer = new char[1024];
while (true){
int len = reader.read(buffer);
if(len == -1){
break;
}
// 把这一段读到的结果, 放到 StringBuilder 中
stringBuilder.append(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}
return stringBuilder.indexOf(word) != -1;
}
}