Java 的 IO 流使用了一种装饰器设计模式,它将 IO 流分为底层节点流和上层处理流。本篇重点在如何访问文件与目录、如何以二进制格式和文本格式来读写数据、对象序列化机制、还有 Java7 的 “NIO.2”。
装饰设计模式:当想要对已有的对象进行功能增强时,可以定义类,将已有对象传入,基于已有的功能,并提供加强功能。那么自定义的该类称为装饰类。
装饰类通常会通过构造方法接收被装饰的对象。并基于被装饰的对象的功能,提供更强的功能。
同步?阻塞?
IO 的方式通常分为:
- BIO(同步阻塞):字节流 InputStream、OutputStream,字符流 Reader、Writer;
- NIO(同步非阻塞):多路复用;
- AIO(异步非阻塞):基于事件和回调机制。
首先我们来看一下 File 类,java.io.File 下代表与平台无关的文件和目录,程序操作文件和目录都可以通过 File 类来完成,File 能新建、删除、重命名文件和目录,但是不能访问文件内容本身。如果需要访问文件内容本身则需要使用输入/输出流。File 的常用方法如下:
File file = new File("."); // 以当前路径来创建一个File对象
String name = file.getName(); // 获取文件名
String parent = file.getParent(); // 获取相对路径的父路径
file.getAbsoluteFile(); // 获取绝对路径
file.getAbsoluteFile().getParent(); // 获取上一级路径
// 创建临时文件
File tempFile = File.createTempFile("temp", ".txt", file); // 在当前路径下创建一个临时文件
tempFile.deleteOnExit(); // 指定当JVM退出时删除该文件
// 创建新文件
File newFile = new File(System.currentTimeMillis() + ".txt"); // 以系统当前时间作为新文件名来创建新文件
boolean b = newFile.exists(); // 判断File对象所对应的文件或目录是否存在,存在返回true
boolean b1 = newFile.createNewFile(); // 以指定newFile对象来创建一个文件
boolean b2 = newFile.mkdir(); // 以newFile对象来创建一个目录,因为newFile已经存在,所以下面方法返回false,即无法创建该目录
下来看一下 IO(输入/输出)的思维导图:
使用处理流的思路:使用处理流包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层 IO 设备、文件交互。
使用处理流的好处:简单;执行效率更高。
注意:处理流的使用必须建立在其他节点流的基础之上。
Java 输入/输出流体系中常用的流分类:
除了这些还有RandomAccessFile,下面将有单独介绍。
1.字符流
1.FileWriter
字符写入流 FileWriter 是专门用于操作文件的 Writer 子类对象。在硬盘上创建一个文件并写入一些数据:
// FileWriter对象一被初始化就必须要明确被操作的文件, 该文件会被创建到指定目录下, 如果已有同名文件,将被覆盖
FileWriter fw = new FileWriter("test.txt");
// 调用write方法, 将字符串写入到流中
fw.write("abcdef");
// 刷新流对象中的缓冲中的数据, 将数据刷到目的地中
//fw.flush();
// 关闭流资源, 关闭之前会刷新一次内部的缓冲中的数据, 将数据刷到目的地中
// 和flush区别: flush刷新后流可以继续使用, close刷新后会将流关闭
fw.close();
对已有文件的数据续写:
// 传递一个true参数, 代表不覆盖已有的文件并在已有文件的末尾处进行数据续写
FileWriter fw = new FileWriter("test.txt",true);
fw.write("abc\r\def");
fw.close();
2.FileReader
字符读取流 FileReader 读取文本文件:
// 创建一个文件读取流对象, 和已存在的文件相关联, 如不存在会发生FileNotFoundException
FileReader fr = new FileReader("test.txt");
// 第一种读取方式: 调用读取流对象的read方法
// read():一次读一个字符, 而且会自动往下读
int ch = 0;
while((ch=fr.read())!=-1) {
System.out.println("ch="+(char)ch);
}
// 第二种读取方式: 通过字符数组进行读取
// 定义一个字符数组, 用于存储读到字符, 该read(char[])返回的是读到字符个数
char[] buf = new char[1024];
int num = 0;
while((num=fr.read(buf))!=-1) {
System.out.println(new String(buf,0,num));
}
fr.close();
3.拷贝文本文件
复制的原理其实就是将 C 盘下的文件数据通过不断的读写存储到 D 盘的一个文件中。复制过程:
FileWriter fw = null;
FileReader fr = null;
try {
fw = new FileWriter("test_copy.txt"); // 创建目的地
fr = new FileReader("test.java"); // 与已有文件关联
char[] buf = new char[1024];
int len = 0;
while ((len = fr.read(buf)) != -1) {
fw.write(buf, 0, len);
}
} catch (IOException e) {
throw new RuntimeException("读写失败");
} finally {
if (fr != null)
try {
fr.close();
} catch (IOException e) {
}
if (fw != null)
try {
fw.close();
} catch (IOException e) {
}
}
4.BufferedWriter
字符流的缓冲流有 BufferedReader 和 BufferedWriter。缓冲区的出现是为了提高流的操作效率而出现的,所以在创建缓冲区之前,必须要先有流对象。字符流缓冲区中提供了一个跨平台的换行符:newLine();
通过字符写入流缓冲区 BufferedWriter 创建 buffered.txt 并写入数据:
// 创建字符写入流对象
FileWriter fw = new FileWriter("buffered.txt");
// 为了提高字符写入流效率, 加入了缓冲技术
// 只要将需要被提高效率的流对象作为参数传递给缓冲区的构造函数即可
BufferedWriter bufw = new BufferedWriter(fw);
for (int x = 1; x < 5; x++) {
bufw.write("abcdef" + x);
bufw.newLine();
bufw.flush();
}
//bufw.flush(); // 记住, 只要用到缓冲区, 就要记得刷新
bufw.close(); // 其实关闭缓冲区, 就是在关闭缓冲区中的流对象
5.BufferedReader
字符读取流缓冲区 BufferedReader:该缓冲区提供了一个一次读一行的方法 readLine,方便于对文本数据的获取。当返回 null 时,表示读到文件末尾。readLine 方法返回的时候只返回回车符之前的数据内容。并不返回回车符。
通过字符读取流缓冲区 BufferedReader 读取数据:
// 创建字符读取流对象
FileReader fr = new FileReader("buffered.txt"); // 创建一个读取流对象和文件相关联
// 为了提高效率, 加入缓冲技术, 将字符读取流对象作为参数传递给缓冲对象的构造函数
BufferedReader bufr = new BufferedReader(fr);
String line = null;
while((line=bufr.readLine())!=null) {
System.out.print(line);
}
bufr.close();
5.通过缓冲区拷贝文本文件
BufferedReader bufr = null;
BufferedWriter bufw = null;
try {
bufr = new BufferedReader(new FileReader("BufferedWriterTest.java"));
bufw = new BufferedWriter(new FileWriter("BufferedWriterTest_copy.txt"));
String line = null;
while ((line = bufr.readLine()) != null) {
bufw.write(line);
bufw.newLine();
bufw.flush();
}
} catch (IOException e) {
throw new RuntimeException("读写失败");
} finally {
try {
if (bufr != null)
bufr.close();
} catch (IOException e) {
throw new RuntimeException("读取关闭失败");
}
try {
if (bufw != null)
bufw.close();
} catch (IOException e) {
throw new RuntimeException("写入关闭失败");
}
}
2.字节流
想要操作字节文件,例如图片数据,字符流就无法满足需求了,这时就要用到字节流。
1.拷贝字节文件
通过字节流拷贝一张图片:
FileOutputStream fos = null;
FileInputStream fis = null;
try {
// 用字节输出流对象创建一个图片文件, 用于存储获取到的图片数据
fos = new FileOutputStream("d:\\pic_copy.bmp");
// 用字节输入流对象和图片关联
fis = new FileInputStream("d:\\pic.bmp");
// 通过循环读写,完成数据的存储
byte[] buf = new byte[1024];
int len = 0;
while ((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
} catch (IOException e) {
throw new RuntimeException("复制文件失败");
} finally {
//关闭资源
try {
if (fis != null)
fis.close();
} catch (IOException e) {
throw new RuntimeException("读取关闭失败");
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
throw new RuntimeException("写入关闭失败");
}
}
2.通过缓冲区拷贝字节文件
通过字节流的缓冲区拷贝 mp4:
BufferedInputStream bufis = new BufferedInputStream(new FileInputStream("c:\\video.Mp4"));
BufferedOutputStream bufos = new BufferedOutputStream(new FileOutputStream("c:\\video_copy.Mp4"));
int by = 0;
while((by=bufis.read())!=-1){
bufos.write(by);
}
bufos.close();
bufis.close();
3.转换流
IO 体系只提供了将字节流向字符流转换的转换流,InputStreamReader 将字节输入流转换成字符输入流,OutputStreamWriter 将字节输出流转换成字节输出流。
下面以获取键盘输入为例来介绍转换流的用法:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 键盘的最常见写法
//BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String line = null;
while((line=br.readLine())!=null) {
if(line.equals("exit")) {
System.exit(1); // 程序退出
}
System.out.println("输入内容为:" + line); // 打印输入内容
}
br.close();
注意:readLine 是字符流 BufferedReader 中的方法,而键盘录入的 read 方法是字节流 InputStream 的。
3.Properties
Properties 是 Hashtable 的子类,具备 map 集合的特点,它里面存储的 key-value 都是字符串,是集合和 IO 技术相结合的集合容器。
Properties 的特点:可以用于 key-value 形式的配置文件,那么在加载数据时,需要数据有固定格式:key = value。
设置和获取元素:
Properties prop = new Properties();
// 设置元素
prop.setProperty("C", "100");
// 获取元素
String value = prop.getProperty("C");
// 遍历元素
Set<String> names = prop.stringPropertyNames();
for (String s : names) {
String value1 = prop.getProperty(s);
}
将流中的数据存储到集合中:
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("test.properties"); // key-value数据
// 将流中的数据加载进集合
prop.load(fis);
String value = prop.getProperty("serverside.log.path");
//prop.list(System.out); // 将信息输出到文件
fis.close();
test.properties 文本内容:
serverside.log.path=/export/Logs/api.example.com
serverside.log.level=INFO
4.对象序列化
序列化机制:允许把内存中的 Java 对象转换成字节序列(与平台无关的二进制流)。
为了让某个类是可序列化的,该类必须实现 Serializable 接口,Java 很多类已经实现 Serializable(只是一个标记接口,实现该接口无须实现任何方法)。
1、序列化:
使用 ObjectOutputStream 将一个对象写入磁盘文件:
try (
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
oos.writeObject(person); // 将person对象写入输出流, person对象必须实现 Serializable 接口
} catch (IOException e) {
e.printStackTrace();
}
2、反序列化:
采用反序列化恢复 Java 对象必须提供该 Java 对象所属类的 class 文件,否则引发 ClassNotFoundException 异常。
try (
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("personObject.txt"))) {
// 从输入流中读取一个Java对象, 并将其强制类型转换为Person类
Person p = (Person) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
5.管道流
Java IO 中的管道为运行在同一个 JVM 中的两个线程提供了通信的能力,所以管道也可以作为数据源以及目标媒介。在 Java 中管道流实现了线程间的数据传送。
注意:当使用两个相关联的管道流时,务必将它们分配给不同的线程,read() 和 write() 调用时会导致流阻塞,这意味着如果你尝试在一个线程中同时进行读和写,可能会导致线程死锁。
读和写分配给不同的线程:
class Read implements Runnable {
private PipedInputStream in; // 管道字节输入流
Read(PipedInputStream in) {
this.in = in;
}
public void run() {
try {
byte[] buf = new byte[1024];
System.out.println("读取前..没有数据阻塞");
int len = in.read(buf);
System.out.println("读到数据..阻塞结束");
String s = new String(buf,0,len);
System.out.println(s);
in.close();
} catch (IOException e) {
throw new RuntimeException("管道读取流失败");
}
}
}
class Write implements Runnable {
private PipedOutputStream out; // 管道字节输出流
Write(PipedOutputStream out) {
this.out = out;
}
public void run() {
try {
System.out.println("开始写入数据,等待5秒后..");
Thread.sleep(5000);
out.write("piped is here".getBytes());
out.close();
} catch (Exception e) {
throw new RuntimeException("管道输出流失败");
}
}
}
开启线程:
PipedInputStream in = new PipedInputStream(); // 管道字节输入流
PipedOutputStream out = new PipedOutputStream(); // 管道字节输出流
in.connect(out); // 关联
Read r = new Read(in);
Write w = new Write(out);
// 开启两个线程
new Thread(r).start();
new Thread(w).start();
运行结果:
读取前..没有数据阻塞
开始写入数据,等待5秒后..
读到数据..阻塞结束
piped is here
6.RandomAccessFile
RandomAccessFile 是 Java IO 体系中功能最丰富的文件内容访问类,RandomAccessFile 不是 IO 体系中的子类,而是直接继承自 Object,但是它是IO包中成员。
RandomAccessFile 提供了众多的方法来访问文件内容,既可以读取文件内容,也可以向文件输出数据。与普通 IO 流不同的是 RandomAccessFile 支持 “随机访问的方式”(内部封装了一个数组,而且通过指针对数组的元素进行操作,可以通过 getFilePointer 获取指针位置,同时可以通过 seek 改变指针的位置),程序可以直接跳转到文件的任意地方来读写数据。所以,如果只需要访问文件部分内容,使用 RandomAccessFile 是更好的选择。
RandomAccessFile 完成读写的原理就是内部封装了字节输入流和输出流。
读文件:
RandomAccessFile raf = new RandomAccessFile("ran.txt", "r");
//调整对象中指针
//raf.seek(8*1);
//跳过指定的字节数
raf.skipBytes(8);
byte[] buf = new byte[4];
raf.read(buf);
String name = new String(buf);
int age = raf.readInt();
raf.close();
写文件:
RandomAccessFile raf = new RandomAccessFile("ran.txt", "rw");
raf.seek(8 * 1); // 移动raf的文件记录指针的位置
System.out.println("当前指针的位置是:" + raf.getFilePointer());
raf.write("zhangsan".getBytes());
raf.writeInt(97); // 读用readInt(),97表示ASCII码,代表字符“a”
raf.close();
7.NIO和AIO
1、NIO(同步非阻塞):
前面介绍的 BufferedReader 时提到一个特征,当 BufferedReader 读取输入流中的数据时,如果没有读到有效数据,程序将会在此处阻塞该进程的执行(使用 InputStream 的 read() 从流中读取数据时,如果数据源没有数据,它也会阻塞该线程),也就是说前面介绍的输入流、输出流都是阻塞式的输入、输出。
从 JDK1.4 开始,Java 提供了很多改进 IO 的新功能,称为 NIO(New IO),新增了许多用于处理输入/输出的类,这些类在 java.nio 包及其子包下。
从 Java7 开始,对 NIO 进行了重大改进,改进主要包括提供了全面的文件 IO 和文件系统访问支持;基于异步 Channel 的 IO。Java7 把这种改进称为 NIO.2。
2、AIO(异步非阻塞):
从 Java7 开始,Java 增加了 AIO 新特性,基本上所有的 Java 服务器都重写了自己的网络框架以通过 NIO 来提高服务器的性能。目前很多的网络框架(如 Mina),大型软件(如 Oracle DB)都宣布自己已经在新版本中支持了 AIO 的特性以提高性能。
AIO 的基本原理:
AIO 主要是针对进程在调用 IO 获取外部数据时,是否阻塞调用进程而言的,一个进程的 IO 调用步骤大致如下:
1)进程向操作系统请求数据;
2)操作系统把外部数据加载到内核的缓冲区中;
3)操作系统把内核的缓冲区拷贝到进程的缓冲区,进程获得数据完成自己的功能 。
当操作系统在把外部数据放到进程缓冲区的这段时间(即第2、3步),如果应用进程是挂起等待状态,那么就是同步 IO,反之,就是异步 IO,也就是 AIO。