JAVA 文件操作原理与实战总结
一. JAVA数据流
IO流是一种顺序读写数据的模式,它的特点是单向流动。
1.1 字节流
- IO流以byte(字节)为最小单位,因此也称为字节流。
- 比如,我们要从磁盘读入一个文件,包含6个字节,就相当于读入了6个字节的数据,这6个字节是按顺序读入的,所以是输入字节流。反过来,我们把6个字节从内存写入磁盘文件,就是输出字节流。
- 在Java中,InputStream代表输入字节流,OuputStream代表输出字节流,这是最基本的两种IO流。
- 字节流抽象类:InputStream/OutputStream
1.2 字符流
- 如果需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char来读写显然更方便,这种流称为字符流。
- Java提供了Reader和Writer表示字符流,字符流传输的最小数据单位是char。
- 假如,把char[]数组
Hi你好
这4个字符用Writer字符流写入文件,并且使用UTF-8编码,得到的最终文件内容是8个字节,英文字符H和i各占一个字节,中文字符你好各占3个字节 - 反过来,我们用Reader读取以UTF-8编码的这8个字节,会从Reader中得到Hi你好这4个字符。
- 因此,Reader和Writer本质上是一个能自动编解码的InputStream和OutputStream。
- 字符流接口:Reader/Writer。
二. 文件对象
2.1 创建文件对象
- 构造File对象时,既可以传入绝对路径,也可以传入相对路径。
- File对象既可以表示文件,也可以表示目录。构造一个File对象时。只有当调用File对象的某些方法的时候,才真正进行磁盘操作。
import java.io.File;
File f = new File("C:\\Windows\\notepad.exe"); // 绝对路径
File f = new File("Documents\\Java"); // 相对路径
File f = new File(".\\Documents\\Java"); // 相对路径
File f = new File("..\\Documents\\Java"); // 相对路径
File f = new File("/home/yzyv/programs/netflow/pom.xml"); // 绝对路径
File f = new File("netflow/pom.xml"); // 相对路径
File f = new File("./netflow/pom.xml"); // 相对路径
File f = new File("../programs/netflow/pom.xml"); // 相对路径
System.out.println(f);
2.2 文件对象的三种路径表示
getPath()
,返回构造方法传入的路径getAbsolutePath()
,返回绝对路径getCanonicalPath
,它和绝对路径类似,但是返回的是规范路径
File f = new File("..");
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());
// 加入在/home/yzy目录下执行程序,结果如下
..
/home/yzy/..
/home
2.3 文件对象的常用方法
加入有文件对象File f = new File("..");
,文件对象的常用方法有如下:
boolean isFile()
:是否是文件;boolean isDirectory()
: 是否是目录;boolean canRead()
:是否可读;boolean canWrite()
:是否可写;boolean canExecute
():是否可执行;为目录时,表示能否列出它包含的文件和子目录。long length()
:文件字节大小。boolean mkdir()
:创建当前File对象表示的目录;boolean mkdirs()
:递归创建当前File对象表示的目录boolean createNewFile()
:创建文件boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功。
2.4 临时文件
// 临时文件创建时指定前缀和后缀
File file = File.createTempFile("tmp-", ".txt");
// 临时文件在JVM退出时自动删除
file.deleteOnExit();
2.5 遍历目录
下列代码表示找出C:\\Windows
目录下以.exe
结尾的文件或目录。
import java.io.File;
import java.io.FilenameFilter;
public class Main {
public static void main(String[] args) throws Exception {
File file = new File("C:\\Windows");
File[] fs = file.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".exe");
}
});
for (File f : fs) {
System.out.println(f);
}
}
}
2.6 递归遍历目录
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class HelloJava {
public static void main(String[] args) throws Exception {
List<String> files = new ArrayList<>();
DirectoryRecursive.listFiles(files, "/Users/yzy/tmpy/python");
for (String filePath : files) {
File file = new File(filePath);
System.out.println(filePath);
System.out.println(file.getName());
}
}
}
class DirectoryRecursive {
public static void listFiles(List<String> files, String filePath) {
File[] currentFiles = new File(filePath).listFiles();
if (currentFiles == null || currentFiles.length == 0) {
return;
}
for (File file : currentFiles) {
if (file.isFile()) {
files.add(file.getAbsolutePath());
} else if (file.isDirectory()) {
listFiles(files, file.getAbsolutePath());
}
}
}
}
2.7 路径操作
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
public class HelloJava {
public static void main(String[] args) throws Exception {
// 拼接路径
Path p = Paths.get("/etc/", "ssh", "sshd_config");
String s = p.toAbsolutePath().toString();
System.out.println(s);
// 转换成File对象
File f = p.toRealPath().toFile();
System.out.println(f);
}
}
三. InputStream
3.1 读取字节流
- InputStream是一个抽象类。
- 抽象方法
read
签名如下,表示读取输入流的下一个字节,并返回字节表示的int值(0~255,一个中文字符返回3个数字)。如果已读到末尾,返回-1表示不能继续读取了。InputStream和OutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源。
public abstract int read() throws IOException;
- FileInputStream是InputStream的一个子类,从文件流中读取数据。
public static void readFile() throws IOException {
InputStream input = new FileInputStream("/etc/profile");
while (true) {
int n = input.read();
if (n == -1) {
break;
}
System.out.println(n);
}
input.close();
}
- 为了保证在发生错误的时候的时候资源能正确的关闭,使用异常捕获
public static void readFile() throws IOException {
try (InputStream input = new FileInputStream("/etc/profile")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入finally并调用close()
}
- 编译器只看
try(resource = ...)
中的对象是否实现了java.lang.AutoCloseable
接口,如果实现了,就自动加上finally语句并调用close()
方法
3.2 缓冲批量读
对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节
int read(byte[] b)
:读取若干字节并填充到byte[]数组,返回读取的字节数int read(byte[] b, int off, int len)
:指定byte[]数组的偏移量和最大填充数
public static void readFile() throws IOException {
try (InputStream input = new FileInputStream("/etc/profile")) {
byte[] buffer = new byte[100];
int n;
while ((n = input.read(buffer)) != -1) {
System.out.println("read " + n + " bytes: " + Arrays.toString(buffer));
}
}
}
3.3 实现类FileInputStream
- 读取文件
3.4 实现类ByteArrayInputStream
- ByteArrayInputStream可以在内存中模拟一个InputStream
public static void readFile() throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
}
}
3.5 实现类ServletInputStream
- 从HTTP请求读取数据,是最终数据源;
3.6 实现类Socket.getInputStream()
- 从TCP连接读取数据,是最终数据源;
3.7 读取字符串
- 只能读取纯文本的文件
public class Main{
public static void main(String[] args) throws Exception {
System.out.println(readAsString());
}
public static String readAsString() throws IOException {
try (InputStream input = new FileInputStream("/etc/profile")) {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char)n);
}
return sb.toString();
}
}
}
四. OutputStream
OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:
public abstract void write(int b) throws IOException;
public static void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("/tmp/hello.txt")) {
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
}
try (OutputStream output = new FileOutputStream("/tmp/hello2.txt")) {
output.write("Hello".getBytes(StandardCharsets.UTF_8));
}
}
4.1 实现类FileOutputStream
略
4.2 实现类ByteArrayOutputStream
- ByteArrayOutputStream可以在内存中模拟一个OutputStream
public static void writeFile() throws IOException {
byte[] data;
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
output.write("Hello".getBytes(StandardCharsets.UTF_8));
data = output.toByteArray();
}
System.out.println(new String(data, StandardCharsets.UTF_8));
}
五. 操作多个文件
- 快速复制文件
public static void writeFile() throws IOException {
// 读取input.txt,写入output.txt
try (InputStream input = new FileInputStream("/tmp/input.txt");
OutputStream output = new FileOutputStream("/tmp/output.txt")) {
input.transferTo(output);
}
}
六. 自定义数据流
- 自定义Stream主要由:FilterInputStream 和 FilterOutStream 实现
- 自定义InputStream或者OutputStream可以自定义更多的功能。下面是一个获取字节数的 InputStream。
import java.io.*;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = "hello, world!".getBytes(StandardCharsets.UTF_8);
try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
System.out.println("Total read " + input.getBytesRead() + " bytes");
}
}
}
class CountInputStream extends FilterInputStream {
private int count = 0;
CountInputStream(InputStream in) {
super(in);
}
public int getBytesRead() {
return this.count;
}
public int read() throws IOException {
int n = in.read();
if (n != -1) {
this.count ++;
}
return n;
}
public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b, off, len);
this.count += n;
return n;
}
}
七. 序列化与反序列化
- 序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
- 一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口。Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
- Java的序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
public class Main {
public static void main(String[] args) throws Exception {
OutputStream buffer1 = new FileOutputStream("/tmp/hello1");
OutputStream buffer2 = new FileOutputStream("/tmp/hello2");
// 序列化
try (ObjectOutputStream output1 = new ObjectOutputStream(buffer1);
ObjectOutputStream output2 = new ObjectOutputStream(buffer2)) {
// 写入基本类型
output1.writeInt(12345);
// 写入实现了Serializable接口的Object
output2.writeObject(new Person(9));
}
// 反序列化
try (ObjectInputStream input1 = new ObjectInputStream(new FileInputStream("/tmp/hello1"));
ObjectInputStream input2 = new ObjectInputStream(new FileInputStream("/tmp/hello2"))) {
int n = input1.readInt();
Person p = (Person) input2.readObject();
System.out.println("n = " + n);
System.out.println("p.getAge() = " + p.getAge());
}
}
}
class Person implements Serializable {
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(int age) {
this.age = age;
}
}
八. Reader与Writer
8.1 Reader
- Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取
- java.io.Reader是所有字符输入流的超类,它最主要的方法是:
public int read() throws IOException;
- 这个方法读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1。
8.2 FileReader
// 单个字符读取
try (Reader reader = new FileReader("/etc/profile", StandardCharsets.UTF_8);) {
int n;
while ((n = reader.read()) != -1) {
System.out.println((char) n);
}
}
// 批量字符读取
try (Reader reader = new FileReader("/etc/hosts", StandardCharsets.UTF_8);) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
8.3 CharArrayReader
- 它的作用实际上是把一个char[]数组变成一个Reader,
try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}
8.4 StringReader
- StringReader可以直接把String作为数据源,它和CharArrayReader几乎一样
try (Reader reader = new StringReader("Hello")) {
}
8.5 InputStreamReader
- 如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。
// 持有InputStream:
InputStream input = new FileInputStream("/etc/profile");
// 变换为Reader:
Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8);
// 简写
try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), StandardCharsets.UTF_8)) {
// TODO
}
8.6 Writer
- Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
- Writer是所有字符输出流的超类,它提供的方法主要有:
- 写入一个字符(0~65535):
void write(int c);
- 写入字符数组的所有字符:
void write(char[] c);
- 写入String表示的所有字符:
void write(String s);
8.7 FileWriter
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}
8.9 CharArrayWriter
try (CharArrayWriter writer = new CharArrayWriter()) {
writer.write(65);
writer.write(66);
writer.write(67);
char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}
8.10 StringWriter
try (Writer writer = new StringWriter()) {
writer.write("Hello World!");
System.out.println(writer.toString());
}
8.11 OutputStreamWriter
- OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器。
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), StandardCharsets.UTF_8)) {
// TODO
}
九. 最佳实践
从Java 7开始,提供了Files和Paths这两个工具类,能极大地方便我们读写文件。
9.1 读取为byte[]
byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));
9.2 读取为字符串
// java11
String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.UTF_8);
// java8
String content = new String(Files.readAllBytes(Paths.get("/path/to/file.txt")));
// 上面的 java8 可能读取中文乱码,指定utf8格式读取可以避免
List<String> lines = Files.readAllLines(Paths.get(filePath), StandardCharsets.UTF_8);
Strings.join("\n", lines)
9.3 按行读取返回List
List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));
9.4 一次读取一行
try (BufferedReader br = new BufferedReader(new FileReader("/etc/profile"));) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
9.5 写入二进制文件
Files.write(Paths.get("/tmp/file.txt"), "data".getBytes());
9.6 写入字符串
Files.write(Paths.get("/tmp/file.txt"), "data");
9.7 按行写入数组
字符串列表按行写入。
List<String> lines = new ArrayList<>();
lines.add("line one");
lines.add("line two");
Files.write(Paths.get("/tmp/file.txt"), lines);
9.8 递归删除目录
/**
* 递归删除目录
*/
public static boolean rm_rf(String path) {
File f = new File(path);
if (f.isDirectory()) {
File[] files = f.listFiles();
if (files != null) {
for (File file : files) {
rm_rf(file.toString());
}
}
}
return f.delete();
}
9.9 注意
Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。