原理
在 Java 程序中,对于数据的输入/输出操作以流(Stream)方式进行
JDK 提供了各种各样的流类,用以获取不同种类的数据
程序中通过标准的方法输入/输出数据
文件
Java 有一个类叫 File,它封装的是文件的文件名,只是内存里面的一个对象
真正的文件是在硬盘上的一块空间,在这个文件里面存放着各种各样的数据
读取文件是通过流的方式来读
对于计算机来说,无论读什么类型的数据,都是以 01 的形式读取的。
可以把文件想象成一个桶,文件里面的数据相当于桶里的水,从桶里取水就是从文件读取数据。
常见的取水方法是用一根管道插到桶里面,在管道另一边打开水龙头,桶里的水就从水龙头里流出来了。
桶里的水是通过这根管道流出来的,因此这根管道就叫流。
Java 的流式输入/输出跟水流的原理是一样的。
当要从文件读取数据时
管道一头连着接收方,一头连着文件
文件里的数据顺着管道流出来。
接收方读取从文件流出来的数据。
当要往文件写入数据时
管道一头连着输入方,一头连着文件
输入方的数据顺着管道流进去。
文件处写入管道流出的数据。
网络
除了从文件读取数据,还可以通过网络
用一根管道把两个机子连接起来。
一边说一句话,通过这个管道流进另一边,反之亦然。
包装
有的时候,一根管道不够用
比如这根管道流过来的水有杂质。
可以在这根管道外面再包一层管道,把杂质给过滤掉。
从程序的角度来讲,从计算机读取到的原始数据都是 01 这种形式的,一个字节一个字节地往外读
这样会有些不合适,比如读取字符的时候,由于一个字符占两个字节,按字节读就可能存在只读了半个字符的情况,这样就会显示为乱码。
这时可以在这根管道的外面再包一层比较强大的管道,这个管道可以把 01 转换成字符串。
这样使用程序读取数据时读到的就不再是 01 这种形式的数据了,而是可以看得懂的字符串。
分类
java.io 包中定义了多个流类型(类或抽象类)来实现输入/输出功能
可以从不同的角度对其进行分类:
-
按数据流的方向不同可以分为输入流和输出流
-
按照处理数据单位不同可以分为字节流和字符流
-
按照功能不同可以分为节点流和处理流
先理解两个概念:
-
字节流:最原始的流,读出来的数据就是 01 格式,只不过它是按照字节来读的,一个字节(Byte)是 8 位(bit),读的时候不是一个位一个位读,而是一个字节一个字节读。
-
字符流:一个字符一个字符地往外读取数据。一个字符是2个字节。
JDK 所提供的所有流类型位于包 java.io内,分别继承自以下四种抽象流类型:
-
输入流:InPutStream(字节流),Reader(字符流)
-
输出流:OutPutStream(字节流),Writer(字符流)
这四个类都是抽象类,可以把这四个类想象成四根不同的管道
一端接着程序,另一端接着数据源
可以通过输出管道从数据源里面往外读数据
也可以通过输入管道往数据源里面输入数据
管道一端插进文件里,一端插进程序里,然后开始读数据。
如果站在文件的角度上,这叫输出。
如果站在程序的角度上,这叫输入。
IO 流的输入输出都是站在程序的角度上的。
节点流和处理流
节点流
节点流就是一根管道直接插到数据源上,直接读取或者写入数据源里的数据。
典型的节点流是文件流
- 文件字节输入流(FileInputStream)
- 文件字节输出流 (FileOutputStream)
- 文件字符输入流(FileReader)
- 文件字符输出流(FileWriter)。
处理流
处理流是包在别的流上面的流,相当于是包到别的管道上面的管道。
节点流
字节流
凡是以 InputStream 结尾的流,都是以字节的形式向程序输入数据
继承自 InputStream 的流都是用于向程序中输入数据,且数据的单位为字节(8bit)
下图中深色为节点流,白色为处理流。
基本方法
/**
* 一个字节一个字节往外读,每读取一个字节,就处理一个字节
* 读取一个字节并以整数的形式返回(0-255)
* 如果返回 -1 就说明已经到了输入流的末尾
*/
int read() throws IOException
/**
* 读取一系列字节并存储到一个数组 buffer
* 返回实际读取的字节数,如果读取前已到输入流的末尾,则返回 -1
*/
int read(byte[] buffer) throws IOException
/**
* 读取 length 个字节
* 并存储到一个字节数组 buffer,从 length 位置开始
* 返回实际读取的字节数,如果读取前已到输入流的末尾返回 -1
*/
int read(byte[] buffer,int offset,int length) throws IOException
/**
* 关闭流释放内存资源
*/
void close() throws IOException
/**
* 跳过 n 个字节不读,返回实际跳过的字节数
*/
long skip(long n) throws IOException
文件字节输入流
FileInputStream
读取 test.txt 文件
内容为:test
// 定义文件输入流
FileInputStream in = null;
// 读取时返回的整数
int b;
try {
in = new FileInputStream("io/test.txt");
// 返回的是一个 int 类型的整数,循环结束的条件就是返回一个值 -1 ,表示此时已经读取到文件的末尾了。
while ((b = in.read()) != -1) {
// 把使用数字表示的汉字和英文字母转换成字符输入
System.out.print((char) b);
}
} catch (FileNotFoundException e) {
System.out.println("系统找不到指定文件!");
// 系统非正常退出
System.exit(-1);
} catch (IOException e1) {
System.out.println("文件读取错误!");
} finally {
// 关闭输入流
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出
test
上面的写法中 FileInputStream
使用后是一定要关闭的,一般的做法都是在 finally
中定义。
这样写法在需要关闭的东西较多时是很繁琐的,JDK 1.7 中新增了 try-with-resource
写法,可以简化以上的写法。
// 读取时返回的整数
int b;
try (FileInputStream in = new FileInputStream("io/test.txt")) {
// 返回的是一个 int 类型的整数,循环结束的条件就是返回一个值 -1 ,表示此时已经读取到文件的末尾了。
while ((b = in.read()) != -1) {
// 把使用数字表示的汉字和英文字母转换成字符输入
System.out.print((char) b);
}
} catch (FileNotFoundException e) {
System.out.println("系统找不到指定文件!");
// 系统非正常退出
System.exit(-1);
} catch (IOException e1) {
System.out.println("文件读取错误!");
}
输出
test
这种写法会自动关闭定义的流,既简介,也不用担心忘记关闭。
这是AutoCloseable
接口提供的语法糖。
我们用的流都实现了这个接口,都支持这种写法。
后续的流都使用这种方式关闭。
文件字节输出流
FileOutputStream
继承自 OutputStream 的流是用于程序中输出数据
数据的单位为字节(8bit)
下图中深色的为节点流,浅色为处理流。
基本方法
/**
* 向输出流中写入一个字节数据,该字节数据为参数b的低 8 位
*/
void write(int b) throws IOException
/**
* 将一个字节类型的数组中的数据写入输出流
*/
void write(byte[] b) throws IOException
/**
* 将一个字节类型的数组中的从指定位置(off)开始的len个字节写入到输出流
*/
void write(byte[] b,int off,int len) throws IOException
/**
* 关闭流释放内存资源
*/
void close() throws IOException
/**
* 将输出流中缓冲的数据全部写出到目的地
*/
void flush() throws IOException
写入 test.txt 的内容到 test1.txt
内容为:test
// 写入时返回的整数
int b;
try (FileInputStream in = new FileInputStream("io/test.txt");
FileOutputStream out = new FileOutputStream("io/test1.txt")) {
// 指明要写入数据的文件,如果指定的路径中不存在 test1.txt 这样的文件,则系统会自动创建一个
while ((b = in.read()) != -1) {
out.write(b);
// 调用 write(int c) 方法把读取到的字符全部写入到指定文件中去
}
} catch (FileNotFoundException e) {
System.out.println("文件读取失败");
// 非正常退出
System.exit(-1);
} catch (IOException e1) {
System.out.println("文件复制失败!");
}
执行后生成 test1.txt 文件,内容为:test
同样,如果是写入字符,也可以使用对应的字符流
字符流
基本方法
/**
* 读取一个字节并以整数的形式返回(0-255)
* 如果返回 -1 就说明已经到了输入流的末尾
*/
int read() throws IOException
/**
* 读取一系列字节并存储到一个数组 buffer
* 返回实际读取的字节数,如果读取前已到输入流的末尾,则返回 -1
*/
int read(byte[] buffer) throws IOException
/**
* 读取 length 个字节
* 并存储到一个字节数组 buffer,从 length 位置开始
* 返回实际读取的字节数,如果读取前以到输入流的末尾返回 -1
*/
int read(byte[] buffer,int offset,int length) throws IOException
/**
* 关闭流释放内存资源
*/
void close() throws IOException
/**
* 跳过 n 个字节不读,返回实际跳过的字节数
*/
long skip(long n) throws IOException
文件字符输入流
FileReader
前面的写法读取英文和数字是没有问题的,因为他们都只占一个字节。
但是读取中文这种占两个字节的字符时就会造成两个字节被拆分读取,这样读取出来的就是乱码了。
将 test.txt 的内容改为:测试,再次执行上面的代码。
输出
æµè¯
这时,就需要字符输入流FileReader
进行读取了。
FileReader in = new FileReader("io/test.txt");
输出
测试
基本方法
/**
* 向输出流中写入一个字节数据,该字节数据为参数 b 的低 16 位
*/
void write(int b) throws IOException
/**
* 将一个字节类型的数组中的数据写入输出流
*/
void write(byte[] b) throws IOException
/**
* 将一个字节类型的数组中的从指定位置(off)开始的 len 个字节写入到输出流
*/
void write(byte[] b,int off,int len) throws IOException
/**
* 关闭流释放内存资源
*/
void close() throws IOException
/**
* 将输出流中缓冲的数据全部写出到目的地
*/
void flush() throws IOException
文件字符输出流
FileWriter
FileReader in = new FileReader("io/test.txt");
FileWriter out = new FileWriter("io/test1.txt");
处理流
缓冲流
缓冲流要套接在相应的节点流之上,对读写的数据提供了缓冲的功能,提高了读写的效率,同时增加了一些新的方法。
JDK 提供了 4 种缓冲流,常用构造方法如下(size 为自定义缓冲区的大小)
BufferedReader(Reader in)
BufferedReader(Reader in, int size)
BufferedWriter(Writer out)
BufferedWriter(Writer out, int size)
BufferedInputStream(InputStream in)
BufferedInputStream(InputStream in, int size)
BufferedOutputStream(InputStream in)
BufferedOutputStream(InputStream in, int size)
缓冲输入流读入的数据会先在内存中缓存
BufferedReader 提供了 readLine
方法用于读取一行字符串。
缓冲输出流写出的数据会先在内存中缓存
BufferedWriter 提供了 newLine
用于写入一个行分隔符。
使用 flush
方法将会使内存中的数据立刻写出。
缓冲区
缓冲区(Buffer)就是内存里面的一小块区域。
读写数据时都是先把数据放到这块缓冲区域里面,减少 IO 对硬盘的访问次数,保护硬盘。
字节流
缓冲字节输入流
BufferedInputStream
读取 test.txt 中的内容
test.txt 内容为:0123456789
// 读取时返回的整数
int c;
// 在 FileInputStream 节点流的外面套接一层处理流 BufferedInputStream
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("io/test.txt"))) {
System.out.println((char) bis.read());
System.out.println((char) bis.read());
// 在第 5 个字符处做一个标记
bis.mark(5);
for (int i = 0; i <= 10 && (c = bis.read()) != -1; i++) {
System.out.print((char) c);
}
System.out.println();
/**
* 重新回到原来标记的地方
* 如果读取数量超过了标记的位置,重置的时候会报错
*/
bis.reset();
for (int i = 0; i <= 10 && (c = bis.read()) != -1; i++) {
System.out.print((char) c);
}
} catch (FileNotFoundException e) {
System.out.println("系统找不到指定文件!");
// 系统非正常退出
System.exit(-1);
} catch (IOException e) {
System.out.println("文件读取错误!");
}
缓冲字节输出流
BufferedOutputStream
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("io/test.txt"))) {
for (int i = 97; i < 107; i++) {
out.write(i);
}
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
实际上进行I/O操作的并不是 BufferedInputStream,而是我们传入的 FileInputStream。
BufferedInputStream 虽然有着同样的方法,但是进行了一些额外的处理然后再调用 FileInputStream 的同名方法,这里使用了装饰者模式。
这种模式是父类 FilterInputStream 提供的规范。
BufferedInputStream 支持
reset()
和mark()
操作。当调用
mark()
之后,输入流会以某种方式保留之后读取的readlimit
数量的内容。当读取的内容数量超过
readlimit
则之后的内容不会被保留。当调用
reset()
之后,会使得当前的读取位置回到mark()
调用时的位置。
上面的代码在标记后读取超过标记处再重置并没有报错,说明标记并没有失效。
这是因为重置时,是取readlimit
和 BufferedInputStream 类的缓冲区大小两者中的最大值,而并非完全由readlimit
确定。
查看 BufferedInputStream 构造方法
private static int DEFAULT_BUFFER_SIZE = 8192;
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
BufferedInputStream 默认的缓存值为 8192,前面定义的 5 小于这个值,所以没有取定义值。
想要按定义值设定缓存值需要在 BufferedInputStream 构造方法中传入值。
new BufferedInputStream(new FileInputStream("io/test.txt"), 5)
这样重置时如果检测到超过了标记值就会报错了。
字符流
缓冲字符输入流
BufferedReader
同样,读取字符时也可以用缓冲字符输入流
BufferedReader bis = new BufferedReader(new FileReader("io/test.txt"))
缓冲字符输出流
BufferedWriter
同样,写入字符时也可以用缓冲字符输出流
BufferedWriter out = new BufferedWriter(new FileWriter("io/test.txt"))
转换流
InputStreamReader 和 OutputStreamWriter 用于字节数据到字符数据之间的转换
InputStreamReader 需要和 InputStream 套接。
OutputStreamWriter 需要和 OutputStream 套接。
转换流在构造时可以指定其编码集合。
InputStream isr = new InputStreamReader(System.in, "UTF-8")
转换流非常的有用,它可以把一个字节流转换成一个字符流
转换流有两种,一种叫 InputStreamReader,另一种叫 OutputStreamWriter
InputStream 是字节流,Reader 是字符流,InputStreamReader 就是把 InputStream 转换成 Reader。
OutputStream 是字节流,Writer 是字符流,OutputStreamWriter 就是把 OutputStream 转换成Writer。
把 OutputStream 转换成 Writer 之后就可以一个字符一个字符地通过管道写入数据了,而且还可以写入字符串。
我们如果用一个 FileOutputStream 流往文件里面写东西,得要一个字节一个字节地写进去
但是如果我们在 FileOutputStream 流上面套上一个字符转换流,就可以一个字符串一个字符串地写进去。
字节输入转换流
InputStreamReader
String s;
// System.in 这里的 in 是一个标准的输入流,用来接收从键盘输入的数据
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
// 使用 readLine() 方法把读取到的一行字符串保存到字符串变量 s 中去
s = br.readLine();
while(s != null){
// 把保存在内存s中的字符串打印出来
System.out.println(s.toUpperCase());
// 在循环体内继续接收从键盘的输入
s = br.readLine();
if(s.equalsIgnoreCase("exit")){
// 只要输入exit循环就结束,就会退出
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
字节输出转换流
OutputStreamWriter
try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("io/test.txt", true), "ISO8859_1")){
// 使用 getEncoding() 方法取得当前指定的字符编码
System.out.println(osw.getEncoding());
// 把字符串写入到指定的文件中去
osw.write("测试");
// 如果在调用 FileOutputStream 的构造方法时没有加入 true,那么新加入的字符串就会替换掉原来写入的字符串
osw.write("test");
} catch (Exception e) {
e.printStackTrace();
}
数据流
DataInputStream 和 DataOutputStream 分别继承自 InputStream 和 OutputStream
它们属于处理流,需要分别套接在 InputStream 和 OutputStream 类型的节点流上
DataInputStream 和 DataOutputStream 提供了可以存取与机器无关的 Java 基本数据类型的方法
数据输出流
DataOutputStream
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("io/test.txt"))) {
// 写入一个随机的浮点数
dos.writeDouble(Math.random());
// 写入一个布尔类型
dos.writeBoolean(true);
} catch(Exception e) {
e.printStackTrace();
}
写入的内容
?�E��(
这是二进制格式的内容,无法直接读取其内容。
因此常配合输入流使用进行读取。
数据输入流
DataInputStream
try (DataInputStream dis = new DataInputStream(new FileInputStream("io/test.txt"))) {
// 先写进去的就先读出来,调用 readDouble() 方法读取出写入的随机数
System.out.println(dis.readDouble());
// 后写进去的就后读出来,这里面的读取顺序不能更改位置,否则会打印出不正确的结果
System.out.println(dis.readBoolean());
} catch (Exception e) {
e.printStackTrace();
}
输出的内容
0.033735208464392485
true
打印流
PrintWriter 和 PrintStream 都属于输出流,分别针对与字符和字节
PrintWriter 和 PrintStream 提供了重载的
println
方法用于多种数据类型的输出PrintWriter 和 PrintStream 的输出操作不会抛出异常,用户通过检测错误状态获取错误信息
PrintWriter 和 PrintStream 有自动 flush 功能,传入 autoFlush 参数即可
PrintWriter(Writer out)
PrintWriter(Writer out, boolean autoFlush)
PrintWriter(OutputStream out)
PrintWriter(OutputStream out, boolean autoFlush)
PrintStream(OutputStream out)
PrintStream(OutputStream out, boolean autoFlush)
字节打印流
PrintStream
打印流其实早就在使用了,比如System.out
就是一个 PrintStream,PrintStream 也继承自 FilterOutputStream 类因此依然是装饰器模式。
public final static PrintStream out = null;
传入的输出流,但是它存在自动刷新机制,例如当向 PrintStream 流中写入一个字节数组后自动调用flush()
方法。
PrintStream 永远不会抛出异常,而是使用内部检查机制checkError()
方法进行错误检查。
最方便的是,它能够格式化任意的类型,将它们以字符串的形式写入到输出流。
// 在输出流的外面套接一层打印流,用来控制打印输出
try (PrintStream ps = new PrintStream(new FileOutputStream("io/test.txt"))) {
if (ps != null) {
/**
* 这里调用 setOut() 方法改变了输出窗口,以前写 System.out.print(),默认的输出窗口就是命令行窗口
* 但现在使用 System.setOut(ps) 将打印输出窗口改成了由 ps 指定的文件里面,通过这样设置以后,打印输出时都会在指定的文件内打印输出
* 在这里将打印输出窗口设置到了 test.txt 这个文件里面,所以打印出来的内容会在 test.txt 这个文件里面看到
*/
System.setOut(ps);
}
for (char c = 0; c <= 100; c++) {
// 把世界各国的文字打印到 test.txt 中去
System.out.print(c + "\t");
}
} catch (Exception e) {
e.printStackTrace();
}
字符打印流
PrintWriter
try (PrintWriter writer = new PrintWriter(new FileOutputStream("io/test.txt"))) {
// 其实 System.out 就是一个 PrintStream
writer.println("测试");
} catch (IOException e){
e.printStackTrace();
}
对象流
既然基本数据类型能够读取和写入基本数据类型,对象也是支持的。
ObjectOutputStream 不仅支持基本数据类型,通过对对象的序列化操作,以某种格式保存对象,来支持对象类型的 IO
它不是继承自 FilterInputStream 的
直接将 Object 写入或读出
transient
Java 关键字。意为透明的,用它来修饰的成员变量在序列化的时候不予考虑,也就是当成不存在。
Serializable
实现Serializable接口的类是JDK自动把这个类的对象序列化
Externaliazble
public interface Externalizable extends Serializable
实现 Externalizable 的类则可以自己控制对象的序列化。
建议能让 JDK 自己控制序列化就不要让自己去控制。
定义对象 People
/**
* 凡是要将一个类的对象序列化成一个字节流就必须实现 Serializable 接口。
* Serializable 接口中没有定义方法,Serializable 接口是一个标记性接口,用来给类作标记,只是起到一个标记作用。
* 这个标记是给编译器看的,编译器看到这个标记之后就可以知道这个类可以被序列化。
* @author yifan
*/
public class People implements Serializable {
/**
* 在序列化时,会被自动添加这个属性,它代表当前类的版本,我们也可以手动指定版本。
*/
private static final long serialVersionUID = 1L;
public String name;
public People(String name){
this.name = name;
}
}
对象输出流
ObjectOutputStream
try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("io/test.txt"))){
People people = new People("test");
outputStream.writeObject(people);
outputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
对象输入流
ObjectInputStream
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("io/test.txt"))) {
People people = (People) ois.readObject();
System.out.println(people.name);
} catch (Exception e) {
e.printStackTrace();
}
这里先写入再读取不会存在序列化不匹配问题
但是如果写入后对象发生改变,比如新增了一个属性public int age
,再读取的时候就会报需要序列化错误
这时可以给变动的属性添加 transient 关键字,意为透明的,用它来修饰的成员变量在序列化的时候不予考虑,这样就不会报错了。
在一些 JDK 内部的源码中,也存在大量的 transient 关键字,使得某些属性不参与序列化,取消这些不必要保存的属性,可以节省数据空间占用以及减少序列化时间。