Java I/O 系统
整体思路:先学会使用File类定位文件以及操作文件本身,然后学习 IO 流读写文件数据。
- File 类可以定位文件:进行删除、获取文本本身信息等操作,但是不能读写文件内容;
- IO 流技术可以对硬盘中的文件进行读写。
# 1. File 类
File:它是文件和目录路径名的抽象表示:
- 文件和目录都可以通过 File 封装成对象;
- 对于 File 而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名而已。他可以是存在的,也可以是不存在的。
# 1.1 创建 File 类对象
方法名称 | 说明 |
---|---|
public File(String pathname) | 根据文件路径创建文件对象 |
public File(String parent, String child) | 从父路径名字符串和子路径名字符串创建文件对象 |
public File(File parent, String child) | 根据父路径对应文件对象和子路径名字符串创建文件对象 |
- File 对象可以定位文件和目录。
▲ File.separator
表示系统的路径分隔符(Windows 与 Linux 不同)。
# 1.2 绝对路径和相对路径
绝对路径:从盘符开始
File file1 = new File("D:\\itheima\\a.txt");
相对路径:一般定位模块中的文件的,默认直接到当前工程下的目录寻找文件
File file3 = new File("模块名\\a.txt");
# 1.3 判断文件类型、获取文件信息
方法名称 | 说明 |
---|---|
boolean isDirectory() | 测试此抽象路径名表示的File是否为文件夹 |
boolean isFile() | 测试此抽象路径名表示的File是否为文件 |
boolean exists() | 测试此抽象路径名表示的File是否存在 |
String getAbsolutePath() | 返回此抽象路径名的绝对路径名字符串 |
String getPath() | 将此抽象路径名转换为路径名字符串 |
String getName() | 返回由此抽象路径名表示的文件或文件夹的名称 |
length() | 获取文件的大小:字节个数 |
long lastModified() | 返回文件最后修改的时间毫秒值 |
# 1.4 创建、删除文件
# 创建文件:
方法名称 | 说明 |
---|---|
public boolean createNewFile() | 创建一个新的空的文件 |
public boolean mkdir() | 只能创建一级文件夹 |
public boolean mkdirs() | 可以创建多级文件夹 |
- 当文件已存在时,
createNewFile()
会创建失败并返回 false。 - 当想创建
/resource/img/
目录时,若/resource
不存在,则mkdir
会返回 false。
# 删除文件:
方法名称 | 说明 |
---|---|
public boolean delete() | 删除由此抽象路径名表示的文件或空文件夹 |
delete
在占用该文件时依然可以删除。delete
只能删除空文件夹,不能删除非空文件夹
# 1.5 遍历文件夹
方法名称 | 说明 |
---|---|
String[] list() | 获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 |
File[] listFiles() | 获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回(重点) |
listFiles
方法的注意事项:
- 当调用者不存在时,返回null
- 当调用者是一个文件时,返回null
- 当调用者是一个空文件夹时,返回一个长度为0的数组
- 当调用者是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回
- 当调用者是一个有隐藏文件的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏内容
- 当调用者是一个需要权限才能进入的文件夹时,返回null
# 2. 字符集
计算机可以给人类字符进行编号存储,这套编号规则就是字符集。
# 2.1 常见字符集
# 1)ASCII 字符集
ASCII 使用1个字节存储一个字符,总共可以表示128个字符信息。
# 2)GBK
Windows 系统默认的码表,兼容 ASCII 码表,也包含了几万个汉字,并支持繁体汉字以及部分日韩文字。注意,GBK 是中国的码表,一个中文以两个字节的形式存储。但不包含世界上所有国家的文字。
# 3)Unicode
计算机科学领域里的一项业界字符编码标准,容纳世界上大多数国家的所有常见文字和符号。由于Unicode会先通过 UTF-8,UTF-16,以及 UTF-32 的编码成二进制后再存储到计算机,其中最为常见的就是UTF-8。
- UTF-8 编码后一个中文一般以 3 个字节的形式存储。
- UTF-8 也要兼容 ASCII 编码表。
# 2.2 字符集的编码、解码操作
# String 编码
方法名称 | 说明 |
---|---|
byte[] getBytes() | 使用平台的默认字符集将该 String 编码为一系列字节,将结果存储到新的字节数组中 |
byte[] getBytes(String charsetName) | 使用指定的字符集将该 String 编码为一系列字节,将结果存储到新的字节数组中 |
# String 解码
构造器 | 说明 |
---|---|
String(byte[] bytes) | 通过使用平台的默认字符集解码指定的字节数组来构造新的 String |
String(byte[] bytes, String charsetName) | 通过指定的字符集解码指定的字节数组来构造新的 String |
# 3. I/O 流
# 3.1 I/O 流概述
IO 流也称为输入、输出流,就是用来读写数据的。
- I 表示 intput,是数据从硬盘文件读入内存的过程,称之输入,负责读。
- O 表示 output,是内存程序的数据从内存写出到硬盘文件的过程,称之输出,负责写。
所谓的“输入/输出”是以内存为基准的。
按照流中数据的最小单位,又可以分成字节流(操作所有类型文件)和字符流(操作纯文本文件)。
这些流都是抽象类,不能直接使用。
# 3.2 字节流的使用
InputStream、OutputStream 的一个实现是 FileInputStream 和 FileOutputStream,用于操作文件。
# 3.2.1 文件字节输入流:FileInputStream
构造器 | 说明 |
---|---|
public FileInputStream(File file) | 创建字节输入流管道与源文件对象接通 |
public FileInputStream(String pathname) | 创建字节输入流管道与源文件路径接通 |
方法名称 | 说明 |
---|---|
public int read() | 每次读取一个字节返回,如果字节已经没有可读的返回 -1 |
public int read(byte[] buffer) | 每次读取一个字节数组返回,如果字节已经没有可读的返回 -1 |
示例:【每次读取一个字节】
int c;
while ((c = fs.read()) != -1) {
System.out.print((char) b);
}
2
3
4
- 这种方式其实并不好,效率较低,且对于多字节编码的汉字会出现乱码问题。
示例:【每次读取一个字节数组】
byte[] buf = new byte[3];
int len; // 记录读取的字节数
while ((len = fs.read(buf)) != -1) {
System.out.print(new String(buf, 0, len));
}
2
3
4
5
new String(buf, 0, len)
表示使用 buf 中偏移为 0~len 的字节来构造一个 String,这会使用平台默认的编码方案来解码。同时注意,构造 String 时要 read 了多少就使用多少来创建 String,否则可能会使用到上次读取所用到的字符,因此需要显式传入一个 len。- 性能优于上面的代码,但依然可能会导致多字节编码的汉字出现乱码问题。
Java 标准库提供了一个用于将文件所有字节一次性读入的 API:
方法名称 | 说明 |
---|---|
public byte[] readAllBytes() throws IOException | 直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回 |
- 将文件过大时会抛出异常,但在实际业务中,几乎不会出现读过大的文件,比如读一个 100 G 的文件读入内存没有意义,所以这个 API 绝大部分情况下是可以用的。
- 可以解决多字节编码的问题。
# 3.2.2 文件字节输出流:FileOutputStream
构造器 | 说明 |
---|---|
public FileOutputStream(File file) | 创建字节输出流管道与源文件对象接通 |
public FileOutputStream(File file,boolean append) | 创建字节输出流管道与源文件对象接通,可追加数据 |
public FileOutputStream(String filepath) | 创建字节输出流管道与源文件路径接通 |
public FileOutputStream(String filepath,boolean append) | 创建字节输出流管道与源文件路径接通,可追加数据 |
方法名称 | 说明 |
---|---|
public void write(int a) | 写一个字节出去 |
public void write(byte[] buffer) | 写一个字节数组出去 |
public void write(byte[] buffer , int pos , int len) | 写一个字节数组的一部分出去。 |
示例:
byte[] buf1 = {'a', 97, 100};
os.write(buf1);
byte[] buf2 = "我是中国人".getBytes();
os.write(buf2);
os.write("\r\n".getBytes()); // 写入换行
2
3
4
5
6
7
- 注意写英文和写中文的区别。
- 写入换行时用
\r\n
可以兼容 Windows 和 Linux,而只写\n
只在 Windows 上有效。
方法 | 说明 |
---|---|
flush() | 刷新流,还可以继续写数据 |
close() | 关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据 |
- write 完后一定要 flush 一下,因为 FileOutputStream 内部也有缓冲。
- 操作完数据后要 close。
# 3.3 资源释放方式
# 3.3.1 try-catch-finally
finally:在异常处理时提供 finally 块来执行所有清除操作,比如说 IO 流中的释放资源。被 finally 控制的语句最终一定会执行,除非JVM退出。
try-catch-finally 格式:
try {
FileOutputStream fos = new FileOutputStream("a.txt");
fos.write(97);
fos.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("----- finally -----")
}
2
3
4
5
6
7
8
9
也许会想将 fos.close()
放到 finally 里,但是 close 本身也可能出问题,而且在 fos 在被定义之前也可能出异常导致 fos 为 null,因此使用这种格式写出来的代码很难保证文件正常关闭。
finally 中有 return 的情况
一个易错的情况是:
public static int add(int a, int b) {
try {
int c = a + b;
return c;
} catch (Exception e) {
e.printStackTrace();
return -1;
} finally {
return 100;
}
}
2
3
4
5
6
7
8
9
10
11
当我们调用 add(1, 1)
后返回的是 100 而不是 2。在开发中,不建议在 finally 中出现 return。
# 3.3.2 try-with-resource
JDK 7 和 JDK 9 都简化了资源释放操作。
- 常用的是 JDK 7 的方式。
- JDK 7 以及 JDK 9的
try()
中只能放置资源对象,否则报错。资源都是实现了Closeable/AutoCloseable
接口的类对象
public abstract class InputStream implements Closeable {
...
}
public abstract class OutputStream implements Closeable, Flushable {
...
}
2
3
4
5
6
7
# 3.4 字符流的使用
读取中文是使用字符流更合适,其最小单位是按照单个字符读取的。
Reader、Writer 的一个实现类是 FileReader 和 FileWriter。
# 3.4.1 文件字符输入流:FileReader
构造器 | 说明 |
---|---|
public FileReader(File file) | 创建字符输入流管道与源文件对象接通 |
public FileReader(String pathname) | 创建字符输入流管道与源文件路径接通 |
方法名称 | 说明 |
---|---|
public int read() | 每次读取一个字符返回,如果字符已经没有可读的返回-1 |
public int read(char[] buffer) | 每次读取一个字符数组,返回读取的字符个数,如果字符已经没有可读的返回-1 |
示例:【每次读取一个字符】
int c;
while ((c = fr.read()) != -1) {
System.out.print((char) c);
}
2
3
4
- 读取中文字符不会出现乱码(如果代码编码与文件编码一致)
- 每次读取一个的性能较差
示例:【每次读取一个字符数组】
char[] buf = new char[1024];
int len;
while ((len = fr.read(buf)) != -1) {
String s = new String(buf, 0, len);
System.out.print(s);
}
2
3
4
5
6
- 性能不错,也不会出现中文乱码问题
# 3.4.2 文件字符输出流:FileWriter
构造器 | 说明 |
---|---|
public FileWriter(File file) | 创建字符输出流管道与源文件对象接通 |
public FileWriter(File file,boolean append) | 创建字符输出流管道与源文件对象接通,可追加数据 |
public FileWriter(String filepath) | 创建字符输出流管道与源文件路径接通 |
public FileWriter(String filepath,boolean append) | 创建字符输出流管道与源文件路径接通,可追加数据 |
方法名称 | 说明 |
---|---|
void write(int c) | 写一个字符 |
void write(char[] cbuf) | 写入一个字符数组 |
void write(char[] cbuf, int off, int len) | 写入字符数组的一部分 |
void write(String str) | 写一个字符串 |
void write(String str, int off, int len) | 写一个字符串的一部分 |
void write(int c) | 写一个字符 |
- 如何实现写出去的数据能换行?
fw.write("\r\n")
方法 | 说明 |
---|---|
flush() | 刷新流,还可以继续写数据 |
close() | 关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据 |
写字符示例:
fw.write('中'); // 不会出问题了
fw.write("中文汉字"); // write(String s);
fw.write("中国人".toCharArray()); // write(char[] buf);
fw.flush();
fw.close();
2
3
4
5
6
7
字节流 VS. 字符流
- 字节流适合做一切文件数据的拷贝(音视频,文本)
- 字节流不适合读取中文内容输出
- 字符流适合做文本文件的操作(读,写)
# 3.5 缓冲流
之前学习的字节流可以称为原始流。而缓冲流自带缓冲区、可以提高原始字节流、字符流读写数据的性能。
缓冲流种类:
- 字节缓冲流
- 字节缓冲输入流:BufferedInputStream
- 字节缓冲输出流:BufferedOutputStream
- 字符缓冲流
- 字符缓冲输入流:BufferedReader
- 字符缓冲输出流:BufferedWriter
# 3.5.1 字节缓冲流
**优化原理:**字节缓冲输入流自带了 8KB 缓冲池,以后我们直接从缓冲池读取数据,所以性能较好。字节缓冲输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,写数据性能极高了。
缓冲流只是提供了读写的性能,其功能并未变化。
构造器 | 说明 |
---|---|
public BufferedInputStream(InputStream is) | 可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,从而提高字节输入流读数据的性能 |
public BufferedOutputStream(OutputStream os) | 可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能 |
示例:
try (
// 这里面只能放置资源对象,用完会自动关闭:自动调用资源对象的close方法关闭资源
// 1、创建一个字节输入流管道与原视频接通
InputStream is = new FileInputStream("D:\\resources\\newmeinv.jpeg");
// a.把原始的字节输入流包装成高级的缓冲字节输入流
InputStream bis = new BufferedInputStream(is);
// 2、创建一个字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream("D:\\resources\\newmeinv222.jpeg");
// b.把字节输出流管道包装成高级的缓冲字节输出流管道
OutputStream bos = new BufferedOutputStream(os);
) {
...
}
2
3
4
5
6
7
8
9
10
11
12
13
提示
建议使用字节缓冲输入流、字节缓冲输出流,结合字节数组的方式,目前来看是性能最优的组合。
# 3.5.2 字符缓冲流
# 1)字符缓冲输入流
字符缓冲输入流:BufferedReader
。作用:提高字符输入流读取数据的性能,除此之外多了按照行读取数据的功能。
构造器 | 说明 |
---|---|
BufferedReader(Reader r) | 可以把低级的字符输入流包装成一个高级的缓冲字符输入流管道,从而提高字符输入流读数据的性能 |
新增方法 | 说明 |
---|---|
public String readLine() | 读取一行数据返回,如果读取没有完毕,无行可读返回null |
示例:【按行读取的经典代码】
try (
// 1、创建一个文件字符输入流与源文件接通。
Reader fr = new FileReader("data.txt");
// a、把低级的字符输入流包装成高级的缓冲字符输入流。
BufferedReader br = new BufferedReader(fr);
) {
String line;
while ((line = br.readLine()) != null){
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
9
10
11
12
13
# 2)字符缓冲输出流
字符缓冲输出流:BufferedWriter
。作用:提高字符输出流写取数据的性能,除此之外多了换行功能。
构造器 | 说明 |
---|---|
BufferedWriter(Writer w) | 可以把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能 |
新增方法 | 说明 |
---|---|
public void newLine() | 换行操作 |
# 3.6 转换流
Q:如果代码编码(UTF-8)和文件编码(GBK)不一致,使用字符流直接读取还能不乱码吗?
A:会乱码。字符流下,文件编码和读取的编码必须一致才不会乱码。
# 3.6.1 字符输入转换流
字符输入转换流:InputStreamReader
,可以把原始的字节流按照指定编码转换成字符输入流。
构造器 | 说明 |
---|---|
InputStreamReader(InputStream is) | 可以把原始的字节流按照代码默认编码转换成字符输入流。几乎不用,与默认的 FileReader 一样。 |
InputStreamReader(InputStream is, String charset) | 可以把原始的字节流按照指定编码转换成字符输入流,这样字符流中的字符就不乱码了(重点) 。 |
# 3.6.2 字符输出转换流
Q:如果需要控制写出去的字符使用的编码,怎么办?
- 可以把字符以指定编码获取字节后再使用字节输出流写出去:
“我爱你中国”.getBytes(编码)
- 也可以使用字符输出转换流实现。
字符输出转换流:OutputStreamWriter
,可以把字节输出流按照指定编码转换成字符输出流。
构造器 | 说明 |
---|---|
OutputStreamWriter(OutputStream os) | 可以把原始的字节输出流按照代码默认编码转换成字符输出流。几乎不用。 |
OutputStreamWriter(OutputStream os,String charset) | 可以把原始的字节输出流按照指定编码转换成字符输出流,从而可以指定写出去的字符编码(重点) |
示例:
public static void main(String[] args) throws Exception {
// 1、定义一个字节输出流
OutputStream os = new FileOutputStream("io-app2/src/out03.txt");
// 2、把原始的字节输出流转换成字符输出流
// Writer osw = new OutputStreamWriter(os); // 以默认的UTF-8写字符出去 跟直接写FileWriter一样
Writer osw = new OutputStreamWriter(os , "GBK"); // 指定GBK的方式写字符出去
// 3、把低级的字符输出流包装成高级的缓冲字符输出流。
BufferedWriter bw = new BufferedWriter(osw);
bw.write("中国");
bw.close();
}
2
3
4
5
6
7
8
9
10
11
- 中间字符编码的转换就发生在
osw
这一层处。
# 3.7 打印流
打印流可以实现方便、高效的打印数据到文件中去。打印流一般是指:PrintStream、PrintWriter 两个类。可以实现打印什么数据就是什么数据,例如打印整数97写出去就是97,打印 boolean 的 true,写出去就是 true。
# 3.7.1 PrintStream
构造器 | 说明 |
---|---|
PrintStream(OutputStream os) | 打印流直接通向字节输出流管道 |
PrintStream(File f) | 打印流直接通向文件对象 |
PrintStream(String filepath) | 打印流直接通向文件路径 |
方法 | 说明 |
---|---|
public void print(Xxx xx) | 打印任意类型的数据出去 |
示例:
PrintWriter ps = new PrintWriter("data.txt");
ps.println(97)
ps.close();
2
3
# 3.7.2 PrintWriter
构造器 | 说明 |
---|---|
PrintWriter(OutputStream os) | 打印流直接通向字节输出流管道 |
PrintWriter (Writer w) | 打印流直接通向字符输出流管道 |
PrintWriter (File f) | 打印流直接通向文件对象 |
PrintWriter (String filepath) | 打印流直接通向文件路径 |
方法 | 说明 |
---|---|
void print(Xxx xx) | 打印任意类型的数据出去 |
PrintStream VS. PrintWriter
- 打印(print)数据功能上是一模一样的,都使用方便,性能高效(核心优势)
- 性能高效是因为其内部使用了缓冲,即基于缓冲流构建的。
- PrintStream继承自字节输出流 OutputStream,支持写(write)字节数据的方法。
- PrintWriter继承自字符输出流 Writer,支持写(write)字符数据出去。
注意这里说的“打印(print)”和“写(write)”是不一样的,但我们使用打印流的目的就是为了使用它的打印方法,所以可以认为在使用上 PrintStream 和 PrintWriter 没啥区别。
# 3.7.3 输出语句重定向
可以把输出语句的打印位置改到文件:System.setOut(..)
示例:
public static void main(String[] args) throws Exception {
System.out.println("锦瑟无端五十弦");
System.out.println("一弦一柱思华年");
// 改变输出语句的位置(重定向)
PrintStream ps = new PrintStream("log.txt");
System.setOut(ps); // 把系统打印流改成我们自己的打印流
System.out.println("庄生晓梦迷蝴蝶");
System.out.println("望帝春心托杜鹃");
}
2
3
4
5
6
7
8
9
10
11
# 4. 序列化、反序列化
对象序列化:把内存中的对象存储到磁盘文件中去。使用到的流是对象字节输出流:
ObjectOutputStream
。对象反序列化:把存储到磁盘文件中去的对象数据恢复成内存中的对象。使用到的流是对象字节输入流:
ObjectInputStream
。
序列化的对象要求实现了序列化接口(Serializable
)。
序列化方法 | 说明 |
---|---|
public ObjectOutputStream(OutputStream out) | 构造器,把低级字节输出流包装成高级的对象字节输出流 |
public final void writeObject(Object obj) | 把对象写出去到对象序列化流的文件中去 |
反序列化方法 | 说明 |
---|---|
public ObjectInputStream(InputStream out) | 构造器,把低级字节输如流包装成高级的对象字节输入流 |
public Object readObject() | 把存储到磁盘文件中去的对象数据恢复成内存中的对象返回 |
# 5. Properties 类
Properties 继承自 HashTable,本质上就是一个 Map,但是我们一般把它不会当集合类使用,因为 HashMap 更好用。
Properties 代表的是一个属性文件,可以把自己对象中的键值对信息存入到一个属性文件中去。
属性文件:后缀是 .properties
结尾的文件,里面的内容都是 key=value,后续做系统配置信息的。
Properties 和 IO 流结合的方法:
构造器 | 说明 |
---|---|
void load(InputStream inStream) | 从输入字节流读取属性列表(键和元素对) |
void load(Reader reader) | 从输入字符流读取属性列表(键和元素对) |
void store(OutputStream out, String comments) | 将此属性列表(键和元素对)写入此 Properties表中,以适合于使用 load(InputStream)方法的格式写入输出字节流 |
void store(Writer writer, String comments) | 将此属性列表(键和元素对)写入此 Properties表中,以适合使用 load(Reader)方法的格式写入输出字符流 |
public Object setProperty(String key, String value) | 保存键值对(put) |
public String getProperty(String key) | 使用此属性列表中指定的键搜索属性值 (get) |
public Set<String> stringPropertyNames() | 所有键的名称的集合 (keySet()) |
通过 Properties 的 store
和 load
方法,我们可以将配置数据存储/加载到 Java 对象中。
# 6. 一个简单的 IO 框架:commons-io
commons-io 是 Apache 开源基金组织提供的一组有关 IO 操作的类库,可以提高 IO 功能开发的效率。该工具包提供了很多有关 IO 操作的类。有两个主要的类 FileUtils
、IOUtils
。
# FileUtils 基本使用
方法名 | 说明 |
---|---|
String readFileToString(File file, String encoding) | 读取文件中的数据, 返回字符串 |
void copyFile(File srcFile, File destFile) | 复制文件 |
void copyDirectoryToDirectory(File srcDir, File destDir) | 复制文件夹 |
其他具体使用参考 Apache commons-io (opens new window)。