java -- IO流

java中对于输入输出相关操作进行了抽象,统称为java的IO流操作。这里,我们在以往java文件读写操作(FileWriterFileReader)基础上,进一步深入学习java的IO流概念。

IO流分类

根据不同的分类标准,我们可以对IO流进行不同的的划分。现暂以IO流向IO操作数据类型IO流功能这三个基准,来对IO流进行大致分类,如下:

按流向分

名称作用举例
输入流读取数据FileReader
输出流写出数据FileWriter

按数据类型分

名称作用举例
字节流以byte为单位读写InputStream OutputStream
字符流以char为单位读写FileReader FileWriter

按功能分(是否直接操作数据源)

名称说明举例
节点流可以直接从数据源或目的地读写数据FileReader
FileInputStream
包装流不直接连接数据源或目的地,是其他流的封装,
目的是简化操作或提高效率
InputStreamReader
PrintWriter

IO流读写操作

按照上面的IO流分类,我们也可以将IO流操作分为字节流操作字符流操作,下面将分别进行介绍:

字节流读写 – 二进制文件搬运工

计算机文件,本质上就是对一连串“0101XXX”等二进制数据的存储。正因为如此,一切文件(不管是视频、音频、图片,还是普通文本文件)都可以通过对底层二进制数据的读写,以实现特定的复制、修改操作。

这里以图片的复制操作为例,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.创建FileInputStream对象和FileOutputStream对象
FileInputstream fis = new FileInputStream("hill.jpg");
FileOutputStream fos = new FileOutStream("hill_copy.jpg");

//2.通过字节数组进行复制
int len;
byte[] bytes = new byte[1024];
while((len = fis.read(bytes)) != -1){
fos.write(bytes, 0, len);
}

//3.关闭InputStream和OutputStream,释放资源
fis.close();
fos.close();

字符流读写 – 文本文件好帮手

特别的,除了像上面那样通过字节流完成读写外,文本文件还可以通过字符流的方式完成读写操作。

这是因为文本文件的二进制存储,都是以特定的的编码方式实现的(例如unicode编码);这意味着当这些二进制数据被读入内存,并存入char型数组时,计算机可以正确地完成相应的编解码工作,不会发生数据丢失的状况。

而普通的二进制文件(比如一张图片),由于其二进制存储并未遵循字符编码规则;一旦其被读入内存,并存入字符数组时,计算机会依照相应的编码规则进行解码操作;这时候就会发生有些二进制bit被误当作编码的标志位而丢弃,造成了数据的损坏。

下面我们对字符的编解码进行相关总结:

编码表

可以简单的理解为:一张字符与数值的映射表。根据这张表,计算机可以将内存中存储的数值所对应的字符进行渲染显示。

最早的编码表是ASCII,其只覆盖了阿拉伯数字、英文字母和一些常用符号,主要供英语使用;随后各国根据本国语言,相继推出了本国的编码表:例如中国的GBK

由于各国编码表不一致,常常出现文件移动到不同编码的计算机后出现乱码现象,于是一种兼容所有编码规则的编码表Unicode出现了。最先推出Unicode编码时,是将2个字节作为每个字符的存储单元,但很快就不够用了;于是乎推出了用4个字节存储每个字符的规则,这种方案虽然解决了存储单元空间不足的问题,但也同时造成了存储空间的大量浪费(比如用4个字节去存储只占一个字节的字符)。

在这样背景下,一种可变单元存储空间的编码规则UTF-8出现了,它本质上还是Unicode,只不过改进了前者的存储规则。

另外我们还要提一下ANSI编码,它又叫本地编码表;和上面提到的编码不同,它并不是一种具体的编码规则,而是本地编码的代指。如果你的本地编码是GBK,那么ANSI就是GBK;如果你的本地编码是UTF-8,那么ANSI就是UTF-8

字符串编码

要保证字符串写入文件或打印到console时不乱码,基本思路是:编解码前后一致,包括字符集与字节长度

1
2
3
4
5
6
7
8
9
10
11
12
//以GBK编码写入数据到文件
String str = "你好吗?";
byte[] bys = str.getBytes("GBK");
FileOutputStream fos = new FileOutputStream("a.txt"); //打开文件时必须以GBK编码打开,才能保证不出现乱码
fos.write(bys);
fos.close();

//读取文件中GBK编码数据,并打印到控制台
FileInputStream fis = new FileInputStream("a.txt");
byte[] bys = new byte[1024];
int len = fis.read(bys);
System.out.println(new String(bys,0,len,"GBK"));//必须以GBK编码进行解码操作,不然会出现乱码

字符流编码

字符流 = 字节流 + 编码

1
2
3
4
5
6
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("b.txt"),"GBK");

String s = "你好呀";
osw.write(s);//这里使用GBK对字符串进行了编码,转为字节流后,然后写入了文件中

osw.close();

相关文本文件操作详情可以参考之前的总结java – 文本文件读写

相关unicode等字符编码可以参考字符编码笔记:ASCII,Unicode 和 UTF-8

常用IO流类介绍

标准输入输出流

有关标准输入输出流,我们在之前的日常编码中经常用到,只不过没有引起我们的关注罢了。

诸如向控制台打印System.out.println("xxxxx")或者是键盘录入new Scanner(System.in),我们都可以发现Systemm类的身影。事实上,System中的类字段out以及in就是我们要谈到的标准输出输入流对象。

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
/**
* The "standard" input stream. This stream is already
* open and ready to supply input data. Typically this stream
* corresponds to keyboard input or another input source specified by
* the host environment or user.
*/
public final static InputStream in = null;

/**
* The "standard" output stream. This stream is already
* open and ready to accept output data. Typically this stream
* corresponds to display output or another output destination
* specified by the host environment or user.
* <p>
* For simple stand-alone Java applications, a typical way to write
* a line of output data is:
* <blockquote><pre>
* System.out.println(data)
* </pre></blockquote>
* <p>
* See the <code>println</code> methods in class <code>PrintStream</code>.
*
* @see java.io.PrintStream#println()
* @see java.io.PrintStream#println(boolean)
* @see java.io.PrintStream#println(char)
* @see java.io.PrintStream#println(char[])
* @see java.io.PrintStream#println(double)
* @see java.io.PrintStream#println(float)
* @see java.io.PrintStream#println(int)
* @see java.io.PrintStream#println(long)
* @see java.io.PrintStream#println(java.lang.Object)
* @see java.io.PrintStream#println(java.lang.String)
*/
public final static PrintStream out = null;

相关JDK源码如上,不管是in还是out都是字节流对象;其中in用于读取键盘录入数据,而out用于向屏幕输出数据。

OutputStreamWriter && InputStreamReader

由于标准输入输出流处理的都是字节流数据,而我们人类可以识别的只能是字符流数据;一旦我们打算将所谓的字符流数据打印到屏幕,或者是把键盘输入的字节流数据保存为文本文件时,都会不可避免地遇到数据流间相互转化的问题。

例如,我们打算将文本文件的字符流数据打印到console,可以采用如下的办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//read file
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
//OutputStream
OutputStream os = System.out;

String line;
while((line = br.readLine()) != null){ //read file content,and print to console
os.write(line.getBytes()); //由于标准输出流对象只能接收字节流,必须得手动将字符流转为字节流
os.write("\n".getBytes());
}

br.close();
os.close();

以上方法的不便之处显而易见,这种手动的转换极容易被忽视,进而导致错误。

从这一痛点出发,java为我们引入了OutputStreamWriter类,帮助我们自动完成字符流向字节流的转换。使用OutputStreamWriter重写以上方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//read file
BufferedReader br = new BufferedReader(new FileReader("a.txt"));

//print file
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

String line = null;
while((line = br.readLine()) != null){ //read file content,and print to console
bw.write(line); //同OutputStream相比,使用OutputStreamWriter我们只需传入想要打印的字符流对象,而完全不要操心字符流与字节流的转换问题
bw.newLine();
}

br.close();
bw.close();

和将文本文件的字符流数据打印到console中类似,我们从键盘录入字节流数据并将其存储在文本文件当中,可以使用InputStreamReader完成字节流向字符流的自动转换。相关代码如下:

使用手动转换

1
2
3
4
5
6
7
8
9
10
11
12
FileWriter fw = new FileWriter("a.txt");
InputStream is = System.in;

byte[] bys = new byte[1024];
int len;
while ((len=is.read(bys)) != -1){
fw.write(new String(bys,0,len)); //这里需要手动将键盘录入的字节流转化为字符流
fw.flush();
}

fw.close();
is.close();

使用InputStreamReader自动转换

1
2
3
4
5
6
7
8
9
10
11
12
BufferedWriter bw = new BufferedWriter(new FileWriter("a.txt"));
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String line;
while ((line=br.readLine()) != null){ //这里直接通过readLine()读出了字符流,而字节流向字符流的转化由InputStreamReader内部完成
bw.write(line);
bw.newLine();
bw.flush();
}

bw.close();
br.close();

打印流

打印流大体可以分为两类:字符打印流PrintWriter和字节打印流PrintStream。和其他IO流不同的是,打印流仅仅存在输出流类,没有对应的输入流类。

由于PrintStream就是我们之前一直谈到到的System.out,这里不再赘述。

事实上,PrintWriter已经完全可以取代PrintStream的角色,两者的区别就在于:

  1. 自动flush,PrintStream一般可以自动刷新,而PrintWriter只有特定方法(如print())才有

  2. PrintStream不能包装一个Writer,即无法读入字符流

之所以现在还存在PrintStream,是因为其出现要早于JDK1.1,并且JDK中大量使用了它,例如System.out;如果弃用PrintSteam会给JDK带来巨大的重构麻烦。

构造方法

至于PrintWriter,它是一个字符流输出类。这里我们一起看一下它的构造函数:

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
 public PrintWriter(File file) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))),
false);
}

public PrintWriter(File file, String csn)
throws FileNotFoundException, UnsupportedEncodingException
{
this(toCharset(csn), file);
}

public PrintWriter(OutputStream out, boolean autoFlush) {
this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);

// save print stream for error propagation
if (out instanceof java.io.PrintStream) {
psOut = (PrintStream) out;
}
}

public PrintWriter(OutputStream out, boolean autoFlush) {
this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);

// save print stream for error propagation
if (out instanceof java.io.PrintStream) {
psOut = (PrintStream) out;
}
}

public PrintWriter(String fileName, String csn)
throws FileNotFoundException, UnsupportedEncodingException
{
this(toCharset(csn), new File(fileName));
}

public PrintWriter(String fileName) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
false);
}

public PrintWriter(Writer out,
boolean autoFlush) {
super(out);
this.out = out;
this.autoFlush = autoFlush;
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}

public PrintWriter(Writer out,
boolean autoFlush) {
super(out);
this.out = out;
this.autoFlush = autoFlush;
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}

private PrintWriter(Charset charset, File file)
throws FileNotFoundException
{
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charset)),
false);
}

PrintWriter基本上都是有参构造函数,传入的参数有File对象、Writer字符输出流对象、以及OutputStream字节输出流对象。其中传入的File对象通过调用new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName)),最终转为了Writer对象;而OutputStream字节输出流对象,通过new BufferedWriter(new OutputStreamWriter(out))也最终转为了Writer对象。

这里我们可以稍微总结一下:PrintWriter由于自身并不具备字符输出功能,因此是一个包装流,它内部通过调用基本流Writer对象完成字符流IO操作。

特有成员函数

这里我们主要介绍一下PrintWriter有关自动换行自动刷新的方法。相关代码如下:

1
2
PrintWriter pw = new PrintWriter(new FileWriter("a.txt"),true);
pw.println("how do u do?");

如上所示,我们可以通过构造函数中的第二个boolean参数手动开启自动刷新功能。另外,同System.out.println()类似,PrintWriter同样可以使用println()实现输出换行的目的。

注意:即使手动开启了自动刷新,也并不是说PrintWriter中的所有方法调用时都有效果。JDK特别指出,只有当调用print()println()以及format()方法时,才存在自动刷新效果

利用PrintWriter进行文件复制

1
2
3
4
5
6
7
8
PrintWriter pw = new PrintWriter(new FileWriter("dest.txt"),true);
BufferedReader br = new BufferedReader(new FileReader("src.txt"));
String line;
while ((line=br.readLine()) != null){
pw.println(line);
}
pw.close();
br.close();

数据操作流

和InputStreamReader、OutputStreamWriter比较类似,都是非字节数组的数据与字节数组之间的转换。不同的是 DataInputStream、DataOutputStream 能够处理的范围更广。

例如:InputStreamReader 只能将输入字节流转化为字符串;而DataInputStream 可以将字节流按输入顺序,依次转为int、boolean、String、char类型数据。

下面分别从各种数据类型与二进制文件的相互转换以及各种数据类型与字节数组的相互转换两方面来加以介绍:

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
/**
* 各种数据类型与二进制文件的相互转换
*/
//构造数据输出流对象
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("a.txt")));

//通过数据输出流对象,写数据
dos.writeBoolean(true);
//以UTF-8编码写字符串
dos.writeUTF("谁解其中味");
dos.writeChar('a');
dos.writeInt(0);
//这个刷新缓存不用忘记写,不然会报java.io.EOFException
dos.flush();

//构造数据输入流对象
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream("a.txt")));

//通过数据输入流对象,读数据;注意:必须按写入顺序读取
boolean flag = dis.readBoolean();
String str = dis.readUTF();
char c = dis.readChar();
int num = dis.readInt();

System.out.println(flag);
System.out.println(str);
System.out.println(c);
System.out.println(num);
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
/**
* 各种数据类型与字节数组的相互转换
*/
//构造数据输出流对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(baos));

//通过数据输出流对象,写数据
dos.writeBoolean(true);
//以UTF-8编码写字符串
dos.writeUTF("谁解其中味");
dos.writeChar('a');
dos.writeInt(0);
//这个刷新缓存不用忘记写,不然会报java.io.EOFException
dos.flush();

//输出字节数组
byte[] src = baos.toByteArray();

//构造数据输入流对象
DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(src)));

//通过数据输入流对象,读数据;注意:必须按写入顺序读取
boolean flag = dis.readBoolean();
String str = dis.readUTF();
char c = dis.readChar();
int num = dis.readInt();

System.out.println(flag);
System.out.println(str);
System.out.println(c);
System.out.println(num);

对象操作流

顾名思义,这是一个用于读写对象的IO流。按照惯例,我们同样可以将其分为两类:对象输入流ObjectInputStream以及对象输出流ObjectOutputStream

下面对这两个类进行详细介绍:

类名构造方法常用成员方法
ObjectOutputStreamObjectOutputStream(OutputStream out)void writeObject(Object obj)
ObjectInputStreamObjectInputStream(InputStream in)Object readObject()

注意事项

需要指出的是,我们在使用对象操作流进行对象读写时,需要注意对象所在类是否实现了Serializable接口,不然会报java.io.NotSerializableException

除此之外,我们在对象类中还需手动指定序列化IDserialVersionUID,以防止读取已经保存的对象流文件时,由于对象所属类的修改,产生的文件中保存的序列化ID和类中自动计算的序列化ID不一致的异常。这种情况下,通常会报java.io.InvalidClassException xxxxxx local class incompatible等异常。

比较稳妥的写法如下:

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
public class ObjectOutputStream{
public static void main(){
ObjectOutputStream oos = new ObjectOutputStream((new FileOutputStream("a.txt")));

Student stu = new Student("ZhangSan",19);
Student stu2 = new Student("LiSi",20);

oos.writeObject(stu);
oos.writeObject(stu2);

oos.close();
}
}

class Student implements Serializable{ //要操作的对象流所在类需要实现Serializable接口
//这里手动指定了类的序列化IDenumerate
private static final long serialVersionUID = 480241723845934527L;
private String name;
private int age;
private String gender;


public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}

最后还需注意的是:我们在上面代码中用对象操作流输出了两个对象到文件中,但当我们需要从文件中读取这些对象时,很自然的会遇到需要确定读取对象个数的问题。比如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));

Object o = ois.readObject();
System.out.println(o);
Object o2 = ois.readObject();
System.out.println(o2);

//Exception in thread "main" java.io.EOFException
Object o3 = ois.readObject();
System.out.println(o3);

ois.close();

在执行到Object o3 = ois.readObject(),会抛出异常java.io.EOFException,原因是此时文件已经读到尾了。和之前读取文件中的字符或字节流不同的是,之前一旦读到文件末尾我们可以通过返回-1的方式获知情况并终止读取操作;而在读取对象输出流文件中却没有这种机制,我们只能通过捕获异常手动处理方式来解决。

事实上,像这种读取多对象输出流文件的问题,我们可以考虑用一个集合存储多个对象,再将集合以对象流的形式写入文件;这样一来,在读取文件时就不用关系读取对象个数的问题了。本着这个思想,将上述代码重构如下:

1
2
3
4
5
6
7
8
9
10
11
ObjectOutputStream oos = new ObjectOutputStream((new FileOutputStream("b.txt")));
ArrayList<Student> stus = new ArrayList<Student>();
stus.add(new Student("WangWu",30));
stus.add(new Student("ZhaoLiu",23));
oos.writeObject(stus);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.txt"));
Object obj = ois.readObject();
System.out.println(obj);
ois.close();

Properties – 配置文件读写能手

一种特殊的双列集合,实现了Map接口,继承了Hashtable。不同于HashMapProperties中的键值对都为String类型。

另外,由于Properties实现了属性集的持久化,故又称之为属性列表。

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Creates an empty property list with no default values.
*/
public Properties() {
this(null);
}

/**
* Creates an empty property list with the specified defaults.
*
* @param defaults the defaults.
*/
public Properties(Properties defaults) {
this.defaults = defaults;
}

如上,Properties提供了两个构造方法;空参的构造方法用于实例化一个空的属性列表,有参的构造方法可以生成一个指定默认值的属性列表。

常用成员方法

由于Properties实现了Map接口,理论上Map的方法其都可以调用。例如增加属性,我们可以使用put(key,value);删除属性,则可以使用remove(key)

Properties类的官方注释并不推荐我们这么做,理由是使用Map接口的put(key,value)方法可以增加非字符串的属性行,这将导致Propertiesstorehesava方法的调用失败。

这里我们将Properties中的常用方法总结如下:

方法名描述
Object setProperty(String key, String value)新增Properties属性行
getProperty(String key)查询指定key的属性值
String getProperty(String key, String defaultValue)查询指定key的属性值,没有则返回默认值

另外,日常工作中我们会经常使用Properties来进行属性文件的读写,因此特将Properties的IO操作总结如下:

使用Properties中的void list(PrintWriter out)void store(Writer writer, String comments)将属性写入文本文件:

​ 使用list()

1
2
3
4
5
6
7
8
9
Properties prop = new Properties();
prop.setProperty("001","LiSi");
prop.setProperty("002","ZhangSan");
prop.setProperty("003","ZhaoSi");
System.out.println(prop);

PrintWriter pw = new PrintWriter("a.txt");
prop.list(pw);
pw.close();

​ 使用store()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Properties prop = new Properties();
prop.setProperty("001","ZhangSan");
prop.setProperty("002","LiSi");
prop.setProperty("003","WangWu");
prop.setProperty("004","ZhaoLiu");
System.out.println(prop);

OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("b.txt"));
FileOutputStream fos = new FileOutputStream("c.txt");
PrintStream ps = System.out;

prop.store(osw,null);
prop.store(fos,null);
prop.store(ps,null);

osw.close();
fos.close();
ps.close();

使用Properties中的void load(Reader reader)从文件中读取属性:

1
2
3
4
5
Properties prop = new Properties();
FileReader fr = new FileReader("a.txt");
prop.load(fr);
fr.close();
System.out.println(prop);