文件IO
什么是文件?
在计算机中,文件可以分成两个类别:
- 文本文件
- 二进制文件
文本文件
文本文件保存的内容都是文本,即字符串.但该字符串并不是直接能被读取的,而是需要字符编码(字符集)来进行解析,常见的码表有:UTF-8,GBK等等.如果把UTF-8字符码文本使用GBk字符码打开就会出现类似于 锟斤拷,烫烫烫等乱码
常见的文件文件有.txt.java.log
二进制文件
二进制文件保存的是二进制数据.无法通过读取字符串的方式进行读取.强制使用文本读取会发现打开的都是乱码
常见的二进制文件有.png.exe.mp4 甚至平常用来编写文件的.word文件也是二进制文件
什么是IO?
I:是 Input(输入)
O:是 Output(输出)
输入输出是一个相对的过程
-
对于人来说:
人通过键盘给电脑传入信息,这个是输出
电脑通过显示器反馈给人信息,这个是输入 -
对于电脑来说:
人通过键盘给电脑传入信息,这个是输入
电脑通过显示器反馈给人信息,这个是输出文件系统操作
java中有一个 File 类.其中封装了操作系统的文件系统API.通过 File 我们可以进行创建,删除,获取路径等操作.但并不能读写文件内的内容,读写是文件内容操作
File 常见 API 介绍
- 构造方法
方法签名|作用
-|-
File(String pathname)|根据字符串路径来创建一个File对象
File(String parent, String child)|根据父亲路径来创建对象
File(File parent, String chind)|根据父亲File对象的路径来创建对象 - 判断方法
方法签名|作用
-|-
exists()|判断当前文件是否存在
isFile()|判断当前文件是不是普通文件
isDirectory()|判断当前文件是不是一个文件夹 - 获取属性方法
方法签名|作用
-|-
getName()|获取文件或文件夹的名字
getParent()|获取文件当前位置
getPath()|获取当前文件路径
getAbsolutePatch()|获取当前文件绝对路径
getCanonicalPatch()|获取当前文件整理后的绝对路径
String[] list()|返回一个字符串数组.包含该文件夹下所有文件和文件夹名
File[] listFiles()|返回一个File对象组.包含该文件夹下所有文件和文件夹的File对象
length()|获取文件大小(字节).不能获取文件夹的大小
lastModified()|获取最近修改的时间戳 - 创建删除方法
方法签名|作用
createNewFile()|当文件不存在时创建新的空文件,成功返回true
mkdir()|创建单级文件夹,父目录不存在会创建失败
mkdirs()|创建多级文件夹,父目录不存在会连同父目录一起创建
delete()|删除文件或文件夹.若文件夹不为空会删除失败
deleteOnExit()|当前对象运行结束后才进行delete()操作 - 重命名复制方法
方法签名|作用
-|-
renameTo(File dest)|将当前文件或文件夹进行重命名操作,也可用于移动操作
代码示例:
大部分代码都同字面意思一样好理解.
此处示例getPath(),getAbsolutePatch(),getCanonicalPatch()三个方法和重命名复制方法
- 获取路径的方法
- 当File 是绝对路径时,这三个方法的打印内容无差别
java
public static void main(String[] args) throws IOException {
File file = new File("E:/code/file");
//获取当前文件路径
System.out.println(file.getPath());
//获取当前文件绝对路径
System.out.println(file.getAbsolutePath());
//获取当前文件整理后的绝对路径
System.out.println(file.getCanonicalPath());
}
输出内容:
E:\code\file
E:\code\file
E:\code\file
- 当 File 是相对路径时:
getPatch()输出的还是相对路径
getAbsoluePath()输出的是绝对路径
getCanonicalPath()输出的是去掉相对路径中的"."的路径
java
public static void main(String[] args) throws IOException {
File file = new File("./code/file");
//获取当前文件路径
System.out.println(file.getPath());
//获取当前文件绝对路径
System.out.println(file.getAbsolutePath());
//获取当前文件整理后的绝对路径
System.out.println(file.getCanonicalPath());
}
输出内容:
.\code\file
E:\code\class-118-java\practice\J20251217.\code\file
E:\code\class-118-java\practice\J20251217\code\file
- 重命名和移动方法
- 重命名
java
public class Main {
public static void main(String[] args) throws IOException {
//创建父目录对象
File parentFile = new File("E:/code/file");
//创建旧文件对象
File oldFile = new File("E:/code/file/old.txt");
//创建新文件对象
File newFile = new File("E:/code/file/new.txt");
//创建父目录
parentFile.mkdirs();
//创建旧文件
oldFile.createNewFile();
//列出重命名前父目录下的所有文件
System.out.print("重命名前的目录文件");
System.out.println(Arrays.toString(new File("E:/code/file/").list()));
//重命名old.txt为new.txt
oldFile.renameTo(newFile);
//列出重命名后父目录下的所有文件
System.out.print("重命名后的目录文件");
System.out.println(Arrays.toString(new File("E:/code/file/").list()));
//运行完毕后删除文件
newFile.deleteOnExit();
}
运行结果:
重命名前的目录文件[old.txt]
重命名后的目录文件[new.txt]
- 移动文件
java
public class Main {
public static void main(String[] args) throws IOException {
//创建旧文件对象
File oldFile = new File("E:/code/file/1.txt");
//创建新文件对象
File newFile = new File("E:/code/file/1/1.txt");
//创建文件目录
new File(newFile.getParent()).mkdirs();
//创建旧文件
oldFile.createNewFile();
//移动前的文件
System.out.println("移动前:");
System.out.print("旧的的文件是否存在:");
System.out.println(oldFile.exists());
System.out.print("新的文件是否存在");
System.out.println(newFile.exists());
//移动1.txt到1/1.txt
oldFile.renameTo(newFile);
//移动后的文件
System.out.println("移动后:");
System.out.print("旧的的文件是否存在:");
System.out.println(oldFile.exists());
System.out.print("新的文件是否存在");
System.out.println(newFile.exists());
//运行完毕后删除文件
newFile.deleteOnExit();
}
运行结果:
移动前:
旧的的文件是否存在:true
新的文件是否存在false
移动后:
旧的的文件是否存在:false
新的文件是否存在true
文件内容操作
针对文件里的数据进行读写操作(只适用于普通文件,不适用目录文件)
目录文件的读写是由操作系统自己负责的
在文件内容操作中,读写是通过流 来实现的
流有两大类别
- 字节流 :针对文件读写的基本操作是字节
用于操作二进制文件 - 字符流 :针对文件读写的基本单位是字符
用于操作文本文件
一个字符是由多个字节构成的
什么是流?
流就像水流一样.一个文件就是一个水桶.读取文件时,就像是把硬盘中的水通过管道流到内存中的水桶里
假设要读取的水桶有1000ml水
那么我们可以读取10次100ml的水
也可以读取100次100ml的水
流读取的特点就是不管你怎么读取,最后读取的总和一定是相同的
字节流的使用
java中提供了两个字节流的核心类
- InputStream 输入字节流类
- OutputStream 输出字节流类
InputStream 输入字节流的使用
我们并不能直接 new InputStream 来创建该实例类.因为InputStream本质上是一个抽象方法.但java中已经写好了有关文件内容操作的类:FileInputStream
| 构造方法 | 作用 |
|---|---|
| new InputStream(String name) | 通过文件路径构造对象 |
| new InputStream(File file) | 通过File对象的路径来构造对象 |
| API | 作用 |
|---|---|
| int read() | 一次读取一个字节 |
| int read(byte[] b) | 一次读取多个字节 |
| int read(byte[] b, int off. int len) | 从off开始读,读取len个元素 |
| void close() | 释放资源 |
int read()|一次读取一个字节
- 为什么这个方法一次只读取一个字节却使用int作为返回类型而不是byte?
- 读取内容与实际不符:java中byte的范围是-128~127.但如果在读取的字节流中恰好有一段11111111的二进制,通过byte强转为有符号的byte会变成-1
- 冲突:若使用byte,由原因1可知会在读取时遇到-1,而-1也正好是文件结束的标志,这会导致一但读取到11111111的二进制就会直接结束读取
而当我们使用int来表示时,对于读取到的0~255范围的值,java会在前面加上24个0来变成一个int类型.如果读取到真正的文件结尾(即-1时),那就返回-1.这样除了读到文件结尾的-1时,才会返回真正的-1
- 说回这个方法:
这个方法并不常用.原因是一次读取一个字节的再面对读取1mb的文件时IO开销太大了(需要操作1048576次).所以更多的是使用int (read(byte[]) b)这个方法 - 代码示例:
java
public class Main {
public static void main(String[] args) {
//提前在该目录下创建对应的文件,内容填写为abc
try(InputStream inputStream = new FileInputStream("E:/code/file/1.txt");) {
while(true) {
int data = inputStream.read();
if(data == -1) {
break;
}
System.out.print((char)data);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
abc
int read(byte[] b)|一次读取多个字节
通过传入一个字节数组,可以达到一次读取多个字节的效果.能够大大减少硬盘的IO操作
- 代码示例:
java
public class Main {
public static void main(String[] args) {
//提前在该目录下创建对应的文件,内容填写为abc
try(InputStream inputStream = new FileInputStream("E:/code/file/1.txt")) {
while(true) {
//data的长度可以自定义
byte[] data = new byte[1024];
int n = inputStream.read(data);
if(n == -1) {
break;
}
for (int i = 0; i < n; i++) {
//不能直接打印内容,得转换为char
//System.out.print(data[i]);
//正确的打印方法
System.out.print((char)data[i]);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
abc
int read(byte[] b, int off. int len)|从off开始读,读取len个元素
我们先看下段的代码
- 代码示例:
java
public static void main5(String[] args) {
//提前在该目录下创建对应的文件,内容填写为abc
try(InputStream inputStream = new FileInputStream("E:/code/file/1.txt")) {
while(true) {
//data的长度可以自定义
byte[] data = new byte[1024];
int n = inputStream.read(data,0,1);
if(n == -1) {
break;
}
for (int i = 0; i < n; i++) {
System.out.print((char)data[i]);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
abc
对于这段代码,如果每次循环只读取从0位置开始的第一个元素,那么这个理应死循环输出a.但实际输出内容却是abc
造成以上的原因是文件指针
InputStraem中有一个叫文件指针的东西.它会记住上次读到了哪里.对于上段代码
- 第一次进入循环时文件指针在0处,读取文件指针的后一位是a.然后文件指针走到下一个未被读取的字节
- 第二次进入循环时文件指针在1处.读取文件指针的后一位是b,然后文件指针走到下一个未被读取的字节
- 第二次进入循环时文件指针在2处.读取文件指针的后一位是c,然后文件指针走到下一个未被读取的字节
- 第二次进入循环时文件指针在3处.读取文件指针的后一位是结束标志(-1),退出循环
所以这个方法看起来虽然和substring很像,但实际运行的并非一个逻辑
- 正确的代码示例:
java
public static void main(String[] args) {
//提前在该目录下创建对应的文件,内容填写为abc
try(InputStream inputStream = new FileInputStream("E:/code/file/1.txt")) {
byte[] data = new byte[1024];
//去掉while循环
int n = inputStream.read(data,0,1);
for (int i = 0; i < n; i++) {
System.out.println((char)data[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
a
void close()|释放资源
通过前面三个方法的代码示例,可能已经忘掉了还需要close().也可能会认为这个colse()方法是可以省略的.毕竟JVM有内存回收机制
但事实是对于IO操作,JVM的内存回收机制是不会回收IO操作的实例的.原因是IO操作是对硬盘打交道,而不是对内存打交道,显然内存和硬盘不是一个东西
因此,我们还是得手动close().close()方法一般放在finally{}代码块中
- 代码示例:
java
public class Main {
public static void main(String[] args) {
//提前在该目录下创建对应的文件,内容填写为abc
InputStream inputStream = null;
try {
inputStream = new FileInputStream("E:/code/file/1.txt");
while (true) {
int data = inputStream.read();
if(data == -1) {
break;
}
System.out.print((char)data);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
那为什么前面三个方法没有使用close()方法?
这段代码对比上述代码我们会发现一处很明显的不一样.创建实例化操作被放在了try{}代码块内容,而前面三个方法的代码都是放在try()参数内部.区别也就在这,如果将实例化操作放在try()参数中,在执行完实例的逻辑后会自动调用close(),这也就是为什么前面的代码没有写colse()的原因.虽然看起来没有释放资源,但其实已经自动释放资源了
OutputStream 输出字节流的使用
和InputStream类似.我们并不能直接 new OutputStream 来创建该实例类.因为OutputStream本质上是一个抽象方法.但java中已经写好了有关文件内容操作的类FileOutputStream:
| 构造方法 | 作用 |
|---|---|
| new FileOutputStream(String name) | 通过字符串路径构造对象.若路径存在会覆盖原本内容 |
| new FileOutputStream(String name, boolean append) | 作用同上,但若append内容为true,则在文件末尾进行追加而不覆盖 |
| new FileOutputStream(File file) | 通过File对象获取路径 |
| API | 作用 |
|---|---|
| int write() | 一次写入一个字节 |
| int write(byte[] b) | 一次写入多个字节 |
| int write(byte[] b, int off. int len) | 从off开始读,写入len个元素 |
| void close() | 释放资源 |
代码示例:
java
public class Main {
public static void main(String[] args) {
try(OutputStream outputStream = new FileOutputStream("E:/code/file/1.txt");
InputStream inputStream = new FileInputStream("E:/code/file/1.txt")){
//写入文件内容
byte[] bytes = {97,98,99};
outputStream.write(bytes);
//写入后读取
while(true) {
int data = inputStream.read();
if(data == -1) {
break;
}
System.out.print((char)data);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
abc
对于上段代码,多次运行后仍然是abc
若把OutputStream的构造参数中添加一个append参数,就会发现每运行一次都会多一段abc的内容
OutputStream和InputStream的细节区分
- 当文件不存在时:
FileInputStream会直接抛出找不到文件的异常
FileOutputStream会自动创建改文件,前提是父级文件夹存在 - 当文件中存在数据时:
FileInputStream不会修改文件
FileOutputSteram默认会直接覆盖原文件,需要手动添加append参数才不会默认覆盖原文件 - flush()机制:
java中为了提高效率写入的数据都会先暂存起来,存到一定程度才会真正写入硬盘.这会导致有些情况如果不调用flush()或不调用close()(close()会自动执行flash()操作),既是程序显示执行完毕,但可能并未写入硬盘
字符流的使用
字符流的本质还是字节流.但字符流会整理读取到的字节,通过编码表将这些字节数据转化为字符数据(也就是我们熟悉的中文)
在java中同样提供了两个字符流的核心类
- Reader 读取字符流
- Writer 写入字符流
Reader 读取字符流的使用
同样的,Reader和InputStream一样.也是一个抽象类.java中主要使用FileReader类来进行实例化
| 构造方法 | 作用 |
|---|---|
| FileReader(String FileName) | 传入一个文件路径 |
| FileReader(File file) | 传入一个File对象来获取路径 |
| FileReader(String FileName, Charset charSet) | 传入一个文件路径并指定其对应编码表(如StandardCharsets.UTF_8) |
若给定的路径无对应文件会直接抛异常
| API | 作用 |
|---|---|
| int read() | 一次读取一个字符 |
| int read(char[] cbuf) | 一次读取多个字符 |
| void close() | 释放资源 |
int read()|一次读取一个字符
java
public class Main {
public static void main(String[] args) {//// 提前在该目录下创建对应的文件 , 内容填写为 你好世界
try(Reader reader = new FileReader("E:/code/file/1.txt")) {
while(true) {
int data = reader.read();
if(data == -1) {
break;
}
System.out.print((char)data);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
你好世界
int read(char[] cbuf)|一次读取多个字符
java
public class Main {
public static void main(String[] args) {//// 提前在该目录下创建对应的文件 , 内容填写为 你好世界
try(Reader reader = new FileReader("E:/code/file/1.txt")) {
while(true) {
char[] data = new char[1024];
int n = reader.read(data);
if(n == -1) {
break;
}
for (int i = 0; i < n; i++) {
System.out.println(data[i]);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
你
好
世
界
Writer 写入字符流的使用
| 构造方法 | 作用 |
|---|---|
| FileWriter(String FileName) | 自动覆盖原文件 |
| FileWriter(String FileName, boolean append) | 若append参数设置为true,则更改为追加模式,不会清空原文件 |
| FileWriter(String fileName, Charset charset) | 指定编码集,但仍然会清空原文件(可再添加一个true解决) |
| API | 作用 |
|---|---|
| void write(intc) | 写入一个字符,虽然传入参数是int,但会根据对应编码转换为对应字节 |
| void write(char[] cbuf) | 写入多个字符 |
| void write(String str) | 写入一个字符串 |
| void write(String str, int off, int len) | 写入字符串中的off索引后的len个字符 |
| void close() | 释放资源 |
void write(intc)|写入一个字符
java
public class Main {
public static void main(String[] args) {
try(Reader reader = new FileReader("E:/code/file/1.txt");
Writer writer = new FileWriter("E:/code/file/1.txt")) {
writer.write('你');
writer.write('好');
writer.write('世');
writer.write('界');
//使用flush()方法,否则无输出
writer.flush();
char[] data = new char[1024];
int n = reader.read(data);
for (int i = 0; i < n; i++) {
System.out.print(data[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
你好世界
void write(char[] cbuf)|写入多个字符
java
public static void main(String[] args) {
try(Reader reader = new FileReader("E:/code/file/1.txt");
Writer writer = new FileWriter("E:/code/file/1.txt")) {
char[] c = {'你','好','世','界'};
writer.write(c);
//使用flush()方法,否则无输出
writer.flush();
char[] data = new char[1024];
int n = reader.read(data);
for (int i = 0; i < n; i++) {
System.out.print(data[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
输出内容:
你好世界
void write(String str, int off, int len)|写入字符串中的off索引后的len个字符
java
public class Main {
public static void main(String[] args) {
try(Reader reader = new FileReader("E:/code/file/1.txt");
Writer writer = new FileWriter("E:/code/file/1.txt")) {
String str = "你好世界";
writer.write(str);
//使用flush()方法,否则无输出
writer.flush();
char[] data = new char[1024];
int n = reader.read(data);
for (int i = 0; i < n; i++) {
System.out.print(data[i]);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出内容:
你好世界