有关输入输出流(IOStream)的内容,分成两个文档。此篇文档主要介绍节点流。
流(Stream)是对输入输出设备的抽象。在Java中,对于数据的输入和输出操作,都是以“流”的方式进行的。
数据以二进制的形式在程序与设备之间流动传输,就像水通过管道在两个数据池间流动一样。根据流动方向的不同,分为输入流(从设备流向程序)、输出流(从程序流向设备)。这里描述的设备,可以是文件、网络、内存等。
流的分类
可以将数据分拆成流,通过流对象对流进行输入和输出。根据分拆前后的对象,可分为节点流和非节点流。
- 节点流,是将原本就是字节或字符的数据分拆成字节流或字符流,再输入或输出。因为分拆前后都是相同的数据,不需要进行转换,因此可以直接读取或写入而不必转换。同时,每个设备都是一个具体的节点。
- 非节点流,与节点流相反,是读取和输出的类型不一致的流,一般不能够直接读取或写入。其中每一种都有一些特殊的功能。
而根据读取数据的形式和输入输出,流可分为以下4个类,位列java.io包内,每种都是一个抽象类。
输入(Input) | 输出(Output) | |
---|---|---|
字节(byte) | InputStream | OutputStream |
字符(char) | Reader | Writer |
字符流通常比字节流更快。
根据设备的不同,使用的实现类也不一样。黑色字为节点流,棕色字为非节点流。
字节流(-InputStream/OutputStream):
- ByteArray-:字节数组。对于输出流,默认一次输出32字节。
- File-:文件流。
- Filter-:
- Buffered-:字节缓冲流。
- Data-:数据流。
- Print-(仅输出):快速输出流。(因为可以直接操作文件,也可归为节点流)
- Object-:对象流,能够将对象进行序列化地输入输出。
- Piped-:管道流。
字符流(-Reader/Writer):
- Buffered-:字符缓冲流。
- CharArray-:字符数组。
- InputSteam/OutputStream-:字节流转字符流,并指定编码。
- File-:文件流。
- Piped-:管道流。
- Print-(仅输出):快速输出流。(同上)
使用流
在代码中,使用流操作数据的的基本步骤是:
- 声明流:对流对象的引用进行声明。
- 创建流:创建一个流对象,或使用其它类内置的流对象。将引用指向流对象。
- 使用流:对流进行使用,使用read()或write()方法将流进行输入或输出。这两个方法有其它两个重载的有参方法。
- read()方法能够读一个字节/字符,read(byte/char_array[])方法能够读一个数组的字符,read(byte/char_array[], begin_index, lenth)方法能够读数组的指定部分。write同理。
-
输入
public void testByteArrayInputStream() throws Exception {
byte[] buf = "abcdefg".getBytes(); // 将字符串转为byte数组
InputStream input = new ByteArrayInputStream(buf); // 构建流
int result = -1;
while ((result = input.read()) != -1) { // 当读到-1即告结束
System.out.print((char) result); // 一次读一个,挨个打印
}
// 也可以一次读多个字符
byte[] b = new byte[6]; // 一次读6个
int length = 0; // 用于获取读取到的个数
while((length = input.read(b)) != -1) { // 将读取的数据存入到数组b中
System.out.println(length); // 读到的个数
System.out.println(Arrays.toString(b)); // 每次读到的内容
}
// 也可以读特定个
input.read(b, 0, b.lenth) // 从0开始读b.lenth个,存储到b中
input.close(); // 关闭流
}
输出
public void testByteWriteStream() throws Exception {
OutputStream os = new ByteArrayOutputStream();
InputStream is = new ByteArrayInputStream(
"ABCBoy".getBytes());
// 创建一个字节数组输入流,读取1个就用字节数组输出1个
int result = -1;
byte[] count = new byte[1024]; // 一次读1024个
while((result = is.read(count)) != -1) {
os.write(result); // 向os写入is读取的字节数
os.write(count); // 向os写入一个数组的字节
}
byte[] bs = ((ByteArrayOutputStream)os).toByteArray();
// 将写入os的数据转byte数组
System.out.println(Arrays.toString(bs)); // 打印
}
使用输入输出流就是使用输入输出流对象。最主要的步骤是:
将某样的东西转为字节数组/字符数组,如果是控制台之类已经转好了就省略此步
- 将数组写入输入流对象,如果是控制台之类自动写入就省略此步
- 将输入流对象发送到另一端,如果无需发送就省略此步
- 接收输入流对象,将输入流对象转为字节数组/字符数组
- 将数组写入输出流对象
- 将输出流对象发送到另一端,如果无需发送就省略此步
-
管道
管道是一种抽象概念,可以从管道中读取流数据,以及向管道中写入流数据。读取和写入的节点都是管道。这里的管道倒不是一个对象,就只是把输入输出流对接起来了而已。
一般使用两个线程来分别管理管道的输入和输出。public static void main(String[] args) throws Exception {
PipedOutputStream os = new PipedOutputStream(); // 输出流
PipedInputStream is = new PipedInputStream(); // 输入流
is.connect(os); // 管道对接
ExecutorService pool = Executors.newCachedThreadPool(); // 线程池
pool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(2000);
byte[] bytes = "ABCBoy".getBytes();
os.write(bytes, true); // 参数2为ture,写入操作为追加而不是覆盖
os.flush();
} catch (Exception e) { e.printStackTrace(); }
}
});
pool.execute(new Runnable() {
public void run() {
try {
byte[] bytes = new byte[1024];
is.read(bytes);
System.out.print(Arrays.toString(bytes)); // 输出ABCBoy
} catch (IOException e) { e.printStackTrace(); }
}
});
}
文件
InputStream inputStream = new FileInputStream("src/1.jpg"); // 相对路径,要读取工作目录下的文件
OutputStream os = new FileOutputStream("src/2.jpg"); // 要写入的文件
byte[] bytes = new byte[1024]; // 每次读1024个
int length = -1;
while((length = inputStream.read(bytes)) != -1) {
os.write(bytes, 0, length);
}
os.flush(); // 将流中的数据强制刷新到目标文件中。每次写入数据都要进行flush
inputStream.close();
os.close();
文件操作:
- exists():判断文件是否存在
- getAbsolutePath():获取绝对路径
- getPath():获取相对路径(相对路径位于工作目录下)
- createNewFile():若文件路径为相对路径,则在项目目录下创建文件,且不会创建不存在的文件夹
- mkdir():只创建一个文件夹,如果路径不存在则不创建
- mkdirs():层级创建文件夹
- deleteOnExit():删除最底层的文件或文件夹
- 输出流写入的文件若不存在则创建;输入流读取的文件不存在则报错。
异常处理
流在使用时会产生IOException需要处理;文件在使用时也可能会产生异常需要处理。一般的处理方式有以下两种:
- 将异常向上抛出。
- 使用try-catch块包裹。但这又会带来一个问题——流若定义在try块中,若出现异常就会跳过后续代码,导致流无法关闭。因此需要把流定义在try外,把关闭流放在finally块中——而关闭流又会抛异常,又要处理空指针的问题。于是处理异常就会变得十分繁琐:
在JDK1.7之后,引入了try-with-resource资源自动释放机制,使得资源在try-catch运行结束后会自动释放:Reader in = null; // 定义放在try外,以便try的使用和finally的关闭
try {
in = new FileReader("D:/1.txt");
in.read();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(in != null) { // 处理空指针,进行判空处理
try {
in.close(); // 关闭流会抛异常,再用一次try-catch
} catch (IOException e) {
e.printStackTrace();
}
}
}
其中try后面括号内的为with语法,能够容纳资源的声明和初始化,多条语句使用分号隔开。try(Reader in = new FileReader("D:/1.txt")) {
in.read();
} catch (IOException e) {
e.printStackTrace();
}