Java IO流
基础概念
IO和流
- 流代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象;
- Java的IO建立在流(stream)之上,输入流读取数据,输出流写入数据;
- 不同的流类会读/写某个特定的数据源;
- 所有的输出/输入流都有相同的基本方法来读取/写入数据;
IO流的分类
按照数据流的方向来分:输入流、输出流,这里的方向、输入输出是相对而言的。
按照处理数据单位来分:字节流、字符流
- 字节流:数据流中最小的数据单元是字节;
- 字符流:数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。
二者的区别:
- 对于处理对象的类型:字节流可以处理一切文件,而字符流只能处理纯文本文件。
- 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件;
- 而字符流一般用于处理纯文本类型的文件,如
txt
文件等,但不能处理图像视频等非文本文件。
- 关于缓冲区的区别:
- 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高;
- 而对于字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。
按功能来分:节点流、处理流
- 节点流:可以从或向一个特定的地方(节点)读写数据,如
FileInputStream
和FileOutputStream
; - 处理流(用来包装节点流):是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。
- 节点流:可以从或向一个特定的地方(节点)读写数据,如
四个基本的抽象流类型
所有的流都继承它们
输入流 | 输出流 | |
---|---|---|
字节流 | 字节输入流InputStream |
字节输出流OutputStream |
字符流 | 字符输入流Reader |
字符输出流Writer |
选择合适的流类
流的功能种类繁多,如何选择出正确且适合当前情景下的流类是很重要的事情,一下将讲一下大致选择思路:
- 首先要确定当前情景是输入还是输出,判断相对简单:如果是从程序写东西到别的地方,那么就选择输出流,反之用输入流;
- 其次是确定当前情景是选择而字节流还是字符流,这个也相对好判断,如果是文件的下载、复制等则选择字节流,而对于文本内容,如果包含中文或其他字符则选择字符流,否则选择字节流;
- 前面两步就可以选出一个合适的节点流了,比如文件字节输入流
FileInputStream
,如果要在此基础上增强功能,那么就在处理流中选择一个合适的即可。
常用的流类有下:
注:粗体标出的类代表节点流,必须直接与指定的物理节点关联;斜体标出的类代表抽象基类,无法直接创建对象实例。
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
装饰者类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回输出流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
常用IO类图如下:
字节流
输出流
Java的基本输出流类是java.io.OutputStream
——public abstract class OutputStream implements Closeable, Flushable
。
OutputStream
的具体子类使用这些方法向某种特定介质中写入数据。
这个类提供了写入数据所需的基本方法:
public abstract void write(int b) throws IOException
,接收一个整数作为参数,将其对应的字节写入到输出流中;public void write(byte b[]) throws IOException
,接收一个字节数组,将其写入到输出流中;public void write(byte b[], int off, int len) throws IOException
,在上述方法的基础上限制了写入的内容,off代表偏移量(即数组开始位置下标),len为写入长度;public void flush() throws IOException
,对缓冲流而言,只有当缓冲内容达到一定量时才会将缓冲区内容写入输出流并清空,该方法强制写入并清空缓冲区;(注:若不知道当前输出流是否有缓冲区时,在关闭流前尽量加上flush()
进行刷新,以保证数据不会遗失在缓冲区)public void close() throws IOException
,关闭输出流。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//当技术一个流的操作时,要通过调用close()方法将其关闭
//在Java 6及更早的版本时的做法
OutputStream fos = null;
try {
fos = new FileOutputStream("C:\\Users\\12061\\Desktop\\1.txt");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//在Java 7引入了“带资源的try”构造(try with resourse),更简洁地完成上述清理
try (OutputStream fos = new FileOutputStream("C:\\Users\\12061\\Desktop\\1.txt")) {
} catch (IOException e) {
e.printStackTrace();
}
//不再需要finally语句,Java会对try参数列表中所有AutoCloseable对象(实现了Closeablae接口的对象)自动调用close
输入流
Java的基本输入流类是java.io.InputStream
——public abstract class InputStream implements Closeable
。
Intream
的具体子类使用这些方法从某种特定介质中读取数据。
这个类提供了将数据读取为原始字节所需的基本方法:
public abstract int read() throws IOException
,从文件中读取一个字节,返回该字节对应的整数,以-1来表示流的结束;(注:read()
方法会等待并阻塞其后任何代码的执行,直到有1个字节的数据可供读取,输入和输出可能很慢,尽量将I/O放在单独的线程中运行)public int read(byte b[]) throws IOException
;public int read(byte b[], int off, int len) throws IOException
,与输出流的write()
方法对应,为提高效率有了这两个read()
方法的重载,返回值代表读取到的字节的个数,以-1来表示流的结束;这里举一个例子,假设现在要从输入流
in
中读取1024个字节,如果当前只有512个字节可用,要读满1024个字节,用循环实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//此实现未考虑数据源字节数不满1014个
int bytesRead = 0; //表示当前已读字节
int bytesToread = 1024; //表示待读取字节
byte[] bytes = new byte[bytesToread];
while (bytesRead < bytesToread) {
bytesRead += in.read(bytes, bytesRead, bytesToread - bytesRead); //in为数据源
}
//修改,修改循环内内容
while (bytesRead < bytesToread) {
int result = in.read(bytes, bytesRead, bytesToread - bytesRead);
if (result == -1) {
break;
}
bytesRead += result;
}public long skip(long n) throws IOException
,读取数据时跳过数据不进行读取;public int available() throws IOException
,用于确定在不堵塞的情况下有多少个字节可被读取,返回可读取的最小字节数;(注:在流的最后,available()
返回0;而read()
的重载返回-1,如果返回的长度为0,它不会注意流的结束而只是返回0)public void close() throws IOException
,关闭流对象。
扩充:InputStream
的三个不常用的方法——允许程序备份和重新读取以及读取的数据(不支持该用法的流类比支持的多),分别为:
public synchronized void mark(int readlimit)
,在读取到某个数据时使用mark()
方法,在之后重读时,使用mark()
方法会将流重置到mark()
的位置,接下来的读取操作将从标记位置开始往后进行;public synchronized void reset() throws IOException
,功能见mark
;public boolean markSupported()
,但不是所有流都支持标记和重置,在使用前应用markSupported()
方法进行检测。
java.io
包中仅有两个输入流类始终支持标记,分别为BufferedInputStream
和ByteArrayInputStream
。
过滤器流
过滤器流可以串连到输入流或者输出流上,在读/写数据时,过滤器流可以修改数据(例如加密或者压缩),或者只是提供额外的方法,将读/写的数据转换为其它格式。
将过滤器串链在一起
过滤器通过其构造方法与流连接,下列代码段将缓冲文件data.txt
输入:
1 | FileInputStream fin = new FileInputStream("data.txt"); |
创建之后,从文件data.txt
中读取数据可能会使用fin
和bin
的read()
方法,注:如果混合调用连接到同一个源的不同流,可能会违反过滤器流的一些隐含约定,大多数清空下,应当只使用链中最后一个过滤器流的对象进行实际的读/写。在代码中尽量避免此bug的出现:
1 | FileInputStream in = new FileInputStream("data.txt"); |
如果必须使用超类中没有声明的过滤器流的其它方法,可以直接使用链式创建:
1 | DataInputStream din = new DataInputStream( |
注:这种连接是永久的,过滤器无法与流断开连接。
字节流的常用子类
InputStream
常用子类
详细说明一下上图中的类:
InputStream
:InputStream
是所有字节输入流的抽象基类,不能被实例化,作为模板而存在的,为所有实现类定义了处理输入流的方法。FileInputSream
:文件输入流,一个非常重要的字节输入流,用于对文件进行读取操作。PipedInputStream
:管道字节输入流,能实现多线程间的管道通信。ByteArrayInputStream
:字节数组输入流,从字节数组byte[]
中进行以字节为单位的读取,及将资源文件都以字节的形式存入到该类中的字节数组中去。ObjectInputStream
:对象输入流,用来提供对基本数据或对象的持久存储。通俗点说,也就是能直接传输对象,通常应用在反序列化中。它也是一种处理流,构造器的入参是一个InputStream
的实例对象。FilterInputStream
:装饰者类,具体的装饰者继承该类,这些类都是处理类,作用是对节点类进行封装,实现一些特殊功能。BufferedInputStream
:缓冲流,对节点流进行装饰,内部会有一个缓存区,用来存放字节,每次都是将缓存区存满然后发送,而不是一个字节或两个字节这样发送,效率更高。BufferedInputStream
类有一个作为缓冲区的保护字节数组,名为buf
。当调用某个缓冲输出流的
read()
方法时,它首先尝试从缓冲区获得请求的数据,只有当缓冲区没有数据时,流才从底层的源中读取数据。从源获取数据时流会读取尽可能多的数据存入到缓冲区中,而不管是否马上需要使用到这些数据,不会被立即用到的数据可以在之后调用read()
时调用。(注:当从本地磁盘读取文件内容时,读取几百字节的数据和1字节的数据的事件花费几乎是相同的;而对于网络连接,这种效果是不明显的,效率的瓶颈往往在于网络传送的速度)BufferedOutputStream
和BufferedInputStream
各有两个构造方法:1
2
3
4public BufferedOutputStream(OutputStream out);
public BufferedOutputStream(OutputStream out, int size);
public BufferedInputStream(InputStream in);
public BufferedInputStream(InputStream in, int size);其中
size
代表设置缓冲区的大小,无size
参数时缓冲区大小默认为8192字节,即8k。DataInputStream
:DataOutputStream
和DataInputStream
类提供了一些方法,可以用二进制格式读/写Java的基本数据类型和字符串,所用的二进制格式主要用于在两个不同的Java程序之间交换数据(网络连接、数据文件、管道或者中间介质等),输出流写入什么数据,输入流就能读取什么数据。(即以什么格式写入便以什么格式读出)DataInputStream
提供了9个读取二进制数据的方法(与DataOutputStream
中的writeBytes()
和writeChars()
无对应,要通过一次读取一个字节或者字符来处理),分别如下:1
2
3
4
5
6
7
8
9public final boolean readBoolean() throws IOException;
public final byte readByte() throws IOException;
public final short readShort() throws IOException;
public final char readChar() throws IOException;
public final int readInt() throws IOException;
public final long readLong() throws IOException;
public final float readFloat() throws IOException;
public final double readDouble() throws IOException;
public final String readUTF() throws IOException;
OutputStream
常用子类
大部分类都与InputStream
类的子类对应,而PrintStream
应避开使用它。
BufferedOutStream
,该类将写入的数据存储在缓冲区中(一个名为buf
的保护字节数组字段,protected byte buf[]
),直到缓冲区满或刷新输出流,再讲缓冲区中数据一次性写入底层输出流。与普通流的效率对比:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65//单词写入单个字节
public class Demo04 {
public static void main(String[] args) throws Exception {
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream("C:/Users/12061/Desktop/1.txt"));
long s = System.currentTimeMillis();
for (int i = 0; i < 2000000; i++) {
bout.write(i);
}
bout.flush();
long e = System.currentTimeMillis();
System.out.println("缓冲流用时:" + (e - s) + "ms");
bout.close();
FileOutputStream fout = new FileOutputStream("C:/Users/12061/Desktop/1.txt");
s = System.currentTimeMillis();
for (int i = 0; i < 2000000; i++) {
fout.write(i);
}
e = System.currentTimeMillis();
System.out.println("普通流用时:" + (e - s) + "ms");
fout.close();
}
}
/*
写入2000000字节时间:
缓冲流用时:48ms
普通流用时:9198ms
*/
//每次写入字节数组
public class Demo04 {
public static void main(String[] args) throws Exception {
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream("C:/Users/12061/Desktop/1.txt"));
long s = System.currentTimeMillis();
byte[] bytes = new byte[1024];
for (int i = 0; i < 2000000; i++) {
int j = i % 1024;
if (i != 0 && j == 0) {
bout.write(bytes);
}
bytes[j] = (byte) (i);
}
long e = System.currentTimeMillis();
System.out.println("缓冲流用时:" + (e - s) + "ms");
bout.close();
FileOutputStream fout = new FileOutputStream("C:/Users/12061/Desktop/1.txt");
s = System.currentTimeMillis();
for (int i = 0; i < 2000000; i++) {
int j = i % 1024;
if (i != 0 && j == 0) {
fout.write(bytes);
}
bytes[j] = (byte) (i);
}
e = System.currentTimeMillis();
System.out.println("普通流用时:" + (e - s) + "ms");
fout.close();
}
}
/*
写入2000000字节时间:
缓冲流用时:16ms
普通流用时:20ms
*/DataOutputStream
和DataInputStream
类提供了一些方法,可以用二进制格式读/写Java的基本数据类型和字符串,所用的二进制格式主要用于在两个不同的Java程序之间交换数据(网络连接、数据文件、管道或者中间介质等),输出流写入什么数据,输入流就能读取什么数据。(即以什么格式写入便以什么格式读出)DataOutputStream
提供了下列11种方法,可以写入特定的Java数据类型;1
2
3
4
5
6
7
8
9
10
11
12public final void writeBoolean(boolean v) throws IOException;
public final void writeByte(int v) throws IOException;
public final void writeShort(int v) throws IOException;
public final void writeChar(int v) throws IOException;
public final void writeInt(int v) throws IOException;
public final void writeLong(long v) throws IOException;
public final void writeFloat(float v) throws IOException;
public final void writeDouble(double v) throws IOException;
public final void writeBytes(String s) throws IOException;
public final void writeChars(String s) throws IOException;
public final void writeUTF(String str) throws IOException;//将字符串本身用UTF-8编码的一个变体进行编码
//应当只与DataInputStream的readUTF()进行相互调用调试程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Demo05 {
public static void main(String[] args) {
try (
DataOutputStream dout = new DataOutputStream(new FileOutputStream("C:/Users/12061/Desktop/1.txt"));
DataInputStream din = new DataInputStream(new FileInputStream("C:/Users/12061/Desktop/1.txt"))
) {
dout.writeBoolean(true);
boolean b = din.readBoolean();
System.out.println(b);
dout.writeUTF("读取UTF字符串");
String s = din.readUTF();
System.out.println(s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
字符流
输出流
Writer
是java.io.OutputStream
类的映射,它是一个抽象类,与OutputStream
类似,Writer
类不直接使用,而是通过它的某个子类以多态的方式使用。
这个类提供了写入数据所需的基本方法:
public void write(int c) throws IOException
,写入单个字符;public void write(char cbuf[]) throws IOException
,写入字符数组;abstract public void write(char cbuf[], int off, int len) throws IOException
,写入字符数组的一部分;public void write(String str) throws IOException
,写入字符串;public void write(String str, int off, int len) throws IOException
,写入字符串的一部分;abstract public void flush() throws IOException
,刷新流;abstract public void close() throws IOException
,关闭流。
输入流
Reader
是java.io.InputStream
类的镜像,描述都差不多只是用于处理字符,不过多赘述。
这个类提供了写入数据所需的基本方法:
public int read() throws IOException
public int read(char cbuf[]) throws IOException
abstract public int read(char cbuf[], int off, int len) throws IOException
public long skip(long n) throws IOException
public boolean markSupported()
public void mark(int readAheadLimit) throws IOException
public void reset() throws IOException
abstract public void close() throws IOException
public boolean ready() throws IOException
,上述大多数方法的用途基本与字节输入流类似,只是操作对象不同。而ready()
方法却是个特例,它与InputStream
中的available()
方法的用途类似,返回的布尔值代表阅读器是否可以无堵塞地读取。
字符流的常用子类
Reader
的常用子类
详细说明一下上图中的类:
FileReader
:用于读取字符文件的阅读器,通过传入文件创建读写器,不能指定字符编码和默认字节缓冲区大小。PipedReader
:管道字符输入流,实现多线程间的管道通信。CharArrayReader
:从Char
数组中读取数据的介质流。StringReader
:从String
中读取数据的介质流。InputStreamReader
:从字节流到字符流的桥梁(InputStreamReader
构造器的一个参数是InputStream
的实现类的对象),它读取字节并使用指定的字符集将其解码为字符,字符集可以通过字符集名称指定,可以显式给定,也可以接受平台的默认字符集。BufferedReader
:从字符输入流中读取文本,设置一个缓冲区来提高效率。字符缓冲流还有两个独特的方法:
BufferedWriter
类newLine()
:写入一个行分隔符,这个方法会自动适配所在系统的行分隔符。BufferedReader
类readLine()
:读取一个文本行。
Writer
的常用子类
Writer
与Reader
结构类似,方向相反,不再赘述。唯一有区别的是,Writer
的子类PrintWriter
,与PrintStream
类似。