文章目录
IO包括Input(输入)和Output(输出)
数据靠近cpu就是输入,远离cpu就是输出
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/98b8760e8f53444f874bcf7899dfc867.png#pic_center)
通过控制台读取数据到内存也是输入,把数据打印显示到控制台上也是输出
文件
我们先来认识狭义上的文件(file)。针对硬盘这种持久化存储的I/O设备,当我们想要进行数据保存时,往往不是保存成⼀个整体,而是独立成⼀个个的单位进行保存,这个独立的单位就被抽象成文件的概念。文件除了有数据内容之外,还有⼀部分信息,例如文件名、文件类型、文件大小等并不作为文件的数据而存在,我们把这部分信息可以视为文件的元信息。
为了方便管理在硬盘上的这些文件。⼀种专门用来存放管理信息的特殊文件诞生了,也就是我们平时所谓文件夹(folder)或者目录(directory)的概念。其实文件夹也是一种文件,称为"目录文件",也是保存在硬盘上
Windows 操作系统上,还有⼀类文件比较特殊,就是平时我们看到的快捷方式(shortcut),这种文件只是对文件本体的⼀种引用而已。其他操作系统上也有类似的概念,例如,软链接(soft link)等。
广义的文件概念是操作系统中把硬件资源/软件资源都抽象成文件(不仅仅是硬盘上存储数据的文件,还有可以操作网卡,鼠标等硬件设备的文件)
在硬盘上,存在很多文件和目录文件,目录文件又存在一定的"嵌套关系",整体是一个N叉树型结构
从盘符开始一层层往下走,最终到达目标文件之后,中间这些节点集合在一起,就构成了"绝对路径",起点是任意节点那就是"相对路径"
在Windows上,节点和节点之间使用 / 或者 \ 进行分割。Linux,Mac,Android,IOS都只能用 / 来分割。推荐使用 / 来分割,在Windows上字符串常量表示路径使用 \ 来分割还需要转义字符
文本文件 vs 二进制文件
分别指代保存被字符集编码的文本和按照标准格式保存的非被字符集编码过的文件。
无论是文本文件还是二进制文件,本质上都是二进制数据构成的,只不过文本文件的二进制数据可以对应码表构成合法的字符,而不是乱码
区分一个文件是文本,还是二进制,非常关键
编程处理的时候,处理方式(写的代码)是不一样的
一个简单粗暴但有效的判别文本文件和二进制文件的方式是使用记事本看一下。如果是乱码,就是二进制文件;如果不是乱码,就是文本文件
记事本就是将文件的二进制数据按照文本的方式来打开
我们可以把照片(二进制文件)拖到记事本中
打开是形如这样的乱码,那就是二进制文件
- 后缀是docx, pptx, xlsx,mp3,mp4,pdf 的文件都属于二进制文件,是给程序看的
docx是富文本文件,不仅仅有文本,还有格式,样式,其他信息,总体还是通过二进制来表示的 - 后缀是md,html, java, c, css的文件都属于文本文件,是给人看的
使用java操作文件
- 针对文件系统进行操作
创建文件,删除文件,创建目录文件,重命名文件…
- 针对文件内容进行操作
读文件,写文件,打开文件,关闭文件
针对文件系统进行操作
Java标准库提供了File类表示一个文件,进一步通过File提供的方法,就可以进行文件系统操作了
构造 File对象的时候,写的路径不一定非得是真实存在的
pathSeparator是Java为了能够跨平台专门提供的变量,如果是 Windows版本的JDK,上述变量值就是 \ , 如果是Linux/Mac版本的JDK,就是 /
其实没啥必要,统一使用 / 即可支持不同的系统
构造方法
签名 | 说明 |
---|---|
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 | getCanonicalPath() | 返回 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() | 判断⽤户是否对文件有可写权限 |
import java.io.File;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
File f = new File("d:/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());
}
}
import java.io.File;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
File f = new File("./test.txt");
//当我们用IDEA写这种相对路径的时候,如果是通过IDEA直接运行程序的话,"基准目录"就是项目所在的目录
//换成其他运行方式就不一定了
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());
}
}
- getAbsolutePath是将工作路径和相对路径进行简单的拼接
- getCanonicalPath是将上述的路径化简之后的结果,能省略的就去掉的
import java.io.File;
public class Test {
public static void main(String[] args) {
File f = new File("./test.txt");
System.out.println(f.exists());
System.out.println(f.isFile());
System.out.println(f.isDirectory());
}
}
createNewFile方法可能会抛出异常,因为创建文件可能会失败:
- 给出的路径非法
- 权限不足
如果文件已经存在了,不会抛出异常,但返回值是false
import java.io.File;
import java.io.IOException;
public class Test {
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());
}
}
import java.io.File;
public class Test {
public static void main(String[] args) {
//文件删除
File f = new File("./test.txt");
f.delete();
System.out.println(f.exists());
}
}
import java.io.File;
public class Test {
public static void main(String[] args) {
File dir = new File("./testDir");
//mkdir只能创建一级目录
dir.mkdir();
System.out.println(dir.isDirectory());
}
}
import java.io.File;
public class Test {
public static void main(String[] args) {
File dir = new File("./testDir/aaa/bbb/ccc");
//mkdirs可以创建多级目录
dir.mkdirs();
System.out.println(dir.isDirectory());
}
}
针对文件内容进行操作
Java通过"流"(stream)进行文件内容操作
像水流一样,比如想接一杯水,可以一次性接完,也可以分两次三次…接完
读取文件也是一样,可以一次性读完,也可以分多次,非常灵活,称为"文件流"
在Java中,用来操作文件内容的"流"是一组类
大体分为字节流(操作以字节为单位的二进制文件)和字符流(操作以字符为单位的文本文件)
一个字符往往对应多个字节,取决于编码方式
字节流
InputStream
InputStream之所以是抽象类,是因为它不仅仅可以对应硬盘的文件,还可以对应控制台,网卡,蓝牙设备…基本可以认为不同的输入设备都可以对应⼀个 InputStream 类,标准库已经提供了一些实现好的具体的子类了,我们现在只关心从文件中读取,所以使用 FileInputStream(表示硬盘文件)
FileInputStream构造方法
签名 | 说明 |
---|---|
FileInputStream(File file) | 利⽤ File 构造⽂件输⼊流 |
FileInputStream(String name) | 利⽤⽂件路径构造⽂件输⼊流(绝对路径/相对路径都可以) |
这个操作就是打开一个文件,如果没有出现异常,那就是打开成功,接下来就可以读取文件内容
修饰符及返回值类型 | 方法签名 | 说明 |
---|---|---|
int | read() | 一次读取⼀个字节的数据,返回读到的数据,返回 -1 代表已经完全读完了 |
int | read(byte[] b) | 最多读取 b.length 字节的数据到 b中,返回实际读到的数量;-1 代表已经读完了 |
int | read(byte[] b, int off, int len) | 从 off 下标开始最多填充len这么长。返回实际读到的数量;-1 代表已经读完了 |
void | close() | 关闭字节流 |
既然无参数版本的read方法一次读一个字节,那么返回类型为啥不是byte,因为在文件末尾要返回-1
而且我们在文档中可以看到字节的范围是0~255,而byte的范围是-128 ~ 127
那为啥不用short呢?
建议大家凡是使用short的场景都使用int;凡是使用float 的场景都使用double
因为计算机发展到现在,空间不再是核心矛盾了,存储设备的成本越来越低,而cpu单次处理的数据变得越来越长
对于 32位cpu,一次就是处理4个字节的数据,此时要是使用short,操作系统内部其实还是要把short转成int再处理
对于 64位cpu,一次就能处理8个字节的数据,相比int,short就更没啥意义了
float的精度比较低,很容易造成较大的误差
这种写法在Java中不常见,属于一种非常典型的C++式写法,用参数来作为函数的返回结果的"输出型参数"。Java中更常见的是用参数表示"输入",用返回值表示"输出"
这个方法会尽可能把数组填满,如果不够填那就能填多少算多少
假设我这个d:/test.txt
路径下的这个文本文件里的内容只有hello
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("d:/test.txt");
while (true) {
int b = inputStream.read();
if(b == -1) {
//读取完毕
break;
}
System.out.printf("0x%x ", b);
}
}
}
16进制的68就是10进制的104,对应ASCII码表就是’h’
换行然后保存
- 0xd是回车,Windows下是让光标回到行首(不会新增一行)
- 0xa是换行,Windows下是新增一行
这两个一起构成了回车换行操作
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("d:/test.txt");
while (true) {
byte[] bytes = new byte[1024];
int n = inputStream.read(bytes);
if(n == -1) break;
for (int i = 0; i < n; i++) {
System.out.printf("0x%x ", bytes[i]);
}
}
}
}
read(byte[])效率比read()高,因为访问硬盘是低效操作,低效操作次数越少,整体的速度就越快
用close关闭文件也是非常关键的操作
如果没关闭文件,大部分情况是感知不到的,但一旦遇到问题就是大事,相当于"定时炸弹"
打开文件的时候,会在操作系统内核 PCB结构体中,给"文件描述符表"添加一个元素,这个元素就存放当前打开的文件的相关信息,而文件描述符表的长度是存在上限的,还不能自动扩容,一直打开文件而不关闭,就会使文件描述符表被占满,一旦被占满,打开新的文件就会失败,网络通信相关的操作也可能受到影响,执行close的时候就会释放掉文件描述符表上对应的元素
如果close前面有异常抛出或者return,此时close就执行不到了
可以用finally的方式解决,但是需要在外面创建一个变量,不太优雅
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
InputStream inputStream = null;
try {
inputStream = new FileInputStream("d:/test.txt");
while (true) {
byte[] bytes = new byte[1024];
int n = inputStream.read(bytes);
if(n == -1) break;
for (int i = 0; i < n; i++) {
System.out.printf("0x%x ", bytes[i]);
}
}
} finally {
inputStream.close();
}
}
}
把流对象的创建写到try( )里面
此时,代码执行出了try { } 之后,就会自动调用 close了。
只有实现Closeable接口的类才能放到try( )里面
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
try (InputStream inputStream = new FileInputStream("d:/test.txt")){
while (true) {
byte[] bytes = new byte[1024];
int n = inputStream.read(bytes);
if(n == -1) break;
for (int i = 0; i < n; i++) {
System.out.printf("0x%x ", bytes[i]);
}
}
}
}
}
OutputStream
OutputStream 同样是⼀个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件,所以使用 FileOutputStream
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class Test {
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("d:/test.txt")) {
//此处写的这几个数字,就是abcd的ASCll码值
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
运行一下程序,打开文件看看
按照写方式打开文件的时候,会把文件原有的内容清空掉(不是write清空的,而是打开操作清空的)
默认的写方式打开会清空,使用"追加写"的方式打开就不会清空原来的内容
在FileOutputStream构造方法中加个true就是追加写的方式。文件原有的内容还会存在,新写入的数据会追加在之前内容的末尾
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class Test {
public static void main(String[] args) {
try (OutputStream outputStream = new FileOutputStream("d:/test.txt", true)) {
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
修饰符及返回值类型 | 方法签名 | 说明 |
---|---|---|
void | write(int b) | 1次写1个字节,参数是int类型 |
void | write(byte[] b) | 1次写若干字节, 将 b 这个数组中的数据全部写入文件中 |
void | write(byte[] b, int off, int len) | 1次写若干字节, 从b 这个数组中 off 下标开始写入文件中,⼀共写 len 个 |
void | close() | 关闭字节流 |
void | flush() | I/O 的速度是很慢的,所以OutputStream为了减少IO的次数,在写数据的时候会将数据先暂时写入内存的⼀个指定区域⾥,直到该区域满了或者其他指定条件时才真正将数据写入设备中,这个区域⼀般称为缓冲区。这导致我们写的数据,很可能会遗留⼀部分在缓冲区中。所以需要在最后或者合适的位置,调⽤ flush(刷新),将数据刷新到设备中。 |
字符流
Reader 与 Writer
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class Test {
public static void main(String[] args) {
try (Reader reader = new FileReader("d:/test.txt")) {
while (true) {
int c = reader.read();
if(c == -1) break;
System.out.print((char)c);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
把文件的内容改成汉字
Java的char类型是两个字节的,而这里每个汉字都是3个字节的(UTF8编码方式)
这里其实是Java对上述数据进行了编码转换。
虽然文件中的原始数据是3个字节一个字符,read方法在读取的时候能够识别文件是UTF8格式。读的是3个字节,返回是char的时候,会把UTF8编码方式转成了Unicode编码方式,而Unicode中,一个汉字就是2字节
在Java中,不同类型内部使用的编码方式是不一样的
char 用Unicode;String默认则是UTF8
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class Test {
public static void main(String[] args) {
try (Writer writer = new FileWriter("d:/test.txt",true)) {
writer.write("世界第一可爱");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
我们经常使用的Scanner
点进in查看发现其实它是InputStream
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class Test {
public static void main(String[] args) throws IOException {
try (InputStream inputStream = new FileInputStream("d:/test.txt")) {
try (Scanner scanner = new Scanner(inputStream)) {
while (scanner.hasNext()) {
System.out.println(scanner.next());
}
}
}
}
}
PrintWriter 类中提供了我们熟悉的 print/println/printf 方法
快速IO模板:
import java.util.*;
import java.io.*;
public class Main
{
public static PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
public static Read in = new Read();
public static void main(String[] args) throws IOException
{
// 写代码
out.close();
}
}
class Read // 自定义快速读入
{
StringTokenizer st = new StringTokenizer("");
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
String next() throws IOException
{
while(!st.hasMoreTokens())
{
st = new StringTokenizer(bf.readLine());
}
return st.nextToken();
}
String nextLine() throws IOException
{
return bf.readLine();
}
int nextInt() throws IOException
{
return Integer.parseInt(next());
}
long nextLong() throws IOException
{
return Long.parseLong(next());
}
double nextDouble() throws IOException
{
return Double.parseDouble(next());
}
}
练习
(1)扫描指定目录,并找到文件名中包含查询词的所有普通文件(不包含目录)
import java.io.File;
import java.util.Scanner;
public class Test {
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();
//2.构造 File 对象,检查路径是否合法
File rootFile = new File(rootPath);
if(!rootFile.isDirectory()) {
System.out.println("输入路径非法!");
return;
}
//3.进行递归搜索
searchFile(rootFile, searchWord);
}
private static void searchFile(File rootFile, String searchWord) {
//1.列出当前目录中有哪些文件
//File 提供了listFiles方法列出目录中包含了哪些文件或目录文件
File[] files = rootFile.listFiles();
if(files == null) return;
//2.遍历目录下的每个结果,进行判定
for (File f: files) {
if(f.isFile()) {
//如果是普通文件,就判定文件名是否包含 查询词
String fileName = f.getName();
if(fileName.contains(searchWord)) System.out.println(f.getAbsolutePath());
//如果是目录,接着递归
else if(f.isDirectory()) searchFile(f, searchWord);
}
}
}
}
如果要搜索的目录是D盘,输入的一定是d:/ (要加 / )
(2)进行普通文件的复制
import java.io.*;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
//1.输入必要信息
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要复制的文件路径: ");
String srcPath = scanner.next();
System.out.println("请输入要复制到的目标路径: ");
String desPath = scanner.next();
//2.验证上述路径是否合法
File srcFile = new File(srcPath);
if(!srcFile.isFile()) {
System.out.println("输入的要复制的文件路径非法!");
return;
}
//不是判定desPath本身是否存在,而是判定它的上级目录是否存在
File desFile = new File(desPath);
if(!desFile.getParentFile().isDirectory()) {
System.out.println("输入的要复制到的目标路径非法!");
return;
}
//3.读写文件
try (InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputStream = new FileOutputStream(desFile)) {
while (true) {
byte[] bytes = new byte[1024];
int n = inputStream.read(bytes);
if(n == -1) break;
outputStream.write(bytes, 0, n);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
拿到上级目录的方式:
前者返回的是字符串路径,后者返回的是File对象。
我们用后者,可以很方便直接地判定其是否为目录
如果文件不存在就需要创建,如果文件存在就需要覆盖
可以用createNewFile
创建文件,也可以通过OutputStream
打开文件自动创建/覆盖文件,而通过InputStream
打开文件则不会创建文件(系统API本身就是这样的)
之所以是用字节流的方式,是因为复制的文件可能是二进制文件
(3)扫描指定目录,并找到文件内容中包含指定字符的所有普通文件
注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下操作
import java.io.*;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的路径: ");
String path = scanner.next();
System.out.println("请输入要查询的词: ");
String searchWord = scanner.next();
File rootFile = new File(path);
if(!rootFile.isDirectory()) {
System.out.println("当前输入的路径非法!");
return;
}
search(rootFile, searchWord);
}
private static void search(File rootFile, String searchWord) {
File[] files = rootFile.listFiles();
if(files == null) return;
for(File f: files) {
if(f.isFile()) matchWord(f, searchWord);
else if(f.isDirectory()) search(f, searchWord);
}
}
//负责针对一个文件进行读取和判定
private static void matchWord(File f, String searchWord) {
try (Reader reader = new FileReader(f)) {
//把读到的结果,构造到一个StringBuilder里
StringBuilder stringBuilder = new StringBuilder();
while (true) {
int c = reader.read();
if(c == -1) break;
stringBuilder.append((char)c);
}
//循环结束,此时文件所有的内容都放入stringBuilder
if(stringBuilder.indexOf(searchWord) >= 0) {
//找到了
System.out.println(f.getAbsolutePath());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
像这样针对文件内容搜索的场景,就需要使用 搜索引擎 这样的技术,平时用百度搜索内容的时候,本质上就是基于文件内容的搜索
搜索引擎公司通过爬虫的方式,把互联网上的网页尽可能爬过来(经过别人同意的情况下),保存在本地。接下来针对本地的文件内容进行解析并构造索引(搜索引擎里的一套数据结构,包含正排索引和倒排索引),之后进行查询的时候,直接通过索引这样的数据结构来查询就快了