I/O流(I/O Streams)
一个I/O流代表一个输入源或者输出目标。一个流可以代表许多不同种类的源和目标,包括磁盘文件,设备,其他程序,以及内存数组。
流支持不同种类的数据,包括简单的字节,基本数据类型,字符串,以及对象。一些流仅是传递数据,还有一些则操作和转换数据。
不论这些流内部如何工作,但对于使用它们的程序来说,流代表一个简单的模型:一个流就是一个数据序列。程序使用输入流(input stream
)从源中读取数据,一次一个单位;使用输出流(output stream
)往目标写数据,一次一个单位。
字节流(Byte Stream)
程序使用字节流来输入输出8位的字节,所有的字节流类都是InputStream
和OutputStream
的子类。
使用字节流
字节流的种类很多,这里我们来演示下文件I/O字节流是如何使用的:
FileInputStream
FileOutputStream
其他字节流的用法也差不多,主要是构造方式不同
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyBytes {
public static void main(String[] args) throws IOException {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("test.txt");
out = new FileOutputStream("out_test.txt");
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
这个程序使用字节流拷贝文本,它循环从输入流中读取并写入输出流,一次一个字节。这里使用一个int变量进行读写,其值是一个8位的字节值。
务必关闭流
流不再需要时,请务必关闭它,这有助于避免严重的资源泄漏。上面的程序使用finally
代码块来确保这一点,我们也可以利用try-with-resources
语句来简化(类似于python的上下文管理):
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyBytes {
public static void main(String[] args) throws IOException {
try(FileInputStream in = new FileInputStream("test.txt");
FileOutputStream out = new FileOutputStream("out_test.txt")
) {
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
}
}
}
通过try语句声明一个或多个资源,确保它们能自动关闭。任何实现了java.lang.AutoCloseable
或者java.io.Closeable
的对象,都可以作为资源使用。
使用场景
字节流是低层I/O,应该避免使用。这里的文本包含的是字符数据,最合适的选择是使用字符流。字节流仅建议用于最基本的I/O,并且所有其他类型的流都是基于字节流的。
字符流(Character Streams)
java使用Unicode编码存储字符的值,字符流I/O自动在unicode编码和本地字符集之间转换。如果是在西方国家,本地字符集通常是8位ASCII编码的超集。
如果暂不考虑国际化,你可以不理会字符集的问题。如果之后要进行国际化适配,你的程序也无需重新编码,具体可以参考 Internationalization 文档。
使用字符流
所有的字符流类都是Reader
和Writer
的子类,和字节流类似,在文件I/O中也有字符流类:
FileReader
FileWriter
下面的程序和字节流的类似:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CopyCharacters {
public static void main(String[] args) throws IOException {
try (FileReader in = new FileReader("test.txt");
FileWriter out = new FileWriter("out_test2.txt");
) {
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
}
}
}
这里使用一个int变量进行读写,其值是一个16位的字符值。
字符流是对字节流的包装,它使用字节流执行物理I/O,然后处理字符和字节之间的翻译。FileReader
利用FileInputStream
,FileWriter
利用FilterOutputStream
。
还有两种通用的字节字符桥接流:InputStreamReader
和OutputStreamWriter
,如果没有合适的字符流满足你的需求,可以考虑试试这种两个通用的实现。
按行I/O
很多时候我们需要按行读写,这里行分隔符可以是:\r\n
, \r
,或者\n
。下面我们修改上面的程序,按行进行读写,这里需要使用两个新的类:
BufferedReader
PrintWriter
import java.io.*;
public class CopyLines {
public static void main(String[] args) throws IOException {
try(BufferedReader in = new BufferedReader(new FileReader("test.txt"));
PrintWriter out = new PrintWriter(new FileWriter("out_test3.txt"))
) {
String l;
while ((l = in.readLine()) != null) {
out.println(l);
}
}
}
}
缓冲流(Buffered Streams)
目前为止我们看到的大部分示例使用的都是非缓冲I/O,这意味着每次读写请求都由系统直接处理。这会导致程序比较低效,因为通常每次请求都会出发磁盘访问,网络活动,或者是其他相对昂贵的操作。
为了减少这种经常性的开销,java实现了缓冲I/O。缓冲输入流从内存的一片缓冲区读取数据,只有缓冲区空了,才会调用原生的输入接口。类似的,缓冲输出流将数据写入缓冲区,只有缓冲区满了,才会调用原生的输出接口。
程序可以将一个非缓冲流转化为一个缓冲流,通过使用包装类,将非缓冲流对象作为缓冲流类的构造参数即可,比如在上面按行读写的例子:
BufferedReader in = new BufferedReader(new FileReader("test.txt"));
BufferedWriter out = new BufferedWriter(new FileWriter("out_test3.txt"));
有4种缓冲流可以用来包装非缓冲的流:
BufferedInputStream
创建输入缓冲字节流BufferedOutputStream
创建输出缓冲字节流BufferedReader
创建输入缓冲字符流BufferedWriter
创建输出缓冲字符流
刷新缓冲流
有时我们不想等缓冲区满了才刷新。一些缓冲输出类的构造方法支持自动刷新,比如:
PrintWriter out = new PrintWriter(new FileWriter("out_test3.txt"), true);
这里的PrintWriter
对象在构造时,指定自动刷新,那么每次在调用println
或者format
方法时,都会自动刷新缓冲区。
如果想手动刷新,可以调用flush
方法,该方法对所有的缓冲输出流都有效。
扫描(Scanning)
Scanner
类型的对象可以切分格式化的输入为词条并根据它们的数据类型将词条进行转化。
切分词条
scanner默认使用空白切分词条(空白字符包括:空格、缩进、换行符,可以通过Character.isWhitespace
方法判断)。我们将之前的程序稍加修改,按词条输出:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Scanner;
public class ScanTest {
public static void main(String[] args) throws IOException {
try (Scanner s = new Scanner(new BufferedReader(new FileReader("test.txt")))) {
while (s.hasNext()) {
System.out.println(s.next());
}
}
}
}
/* 输出如下
In
Xanadu
did
...
sea.
*/
如果想手动指定词条的分割符,可以使用useDelimiter
指定一个正则表达式,比如你想通过逗号分割:
s.useDelimiter(",\\s*");
转化词条
上面的程序将所有的输入词条作为字符串值对待。Scanner
也支持所有java基本数据类型(除了char
),以及BigInteger
和BigDecimal
。另外,Scanner
也可以正确识别使用千位分隔符的数字,比如本地化是US时, "32,767"代表一个整数值。
千位分割符和定点符号与本地化参数有关,因此必须指定本地化参数,否则可能无法正确识别。示例:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Locale;
import java.util.Scanner;
public class ScanSum {
public static void main(String[] args) throws IOException {
double sum = 0;
try (Scanner s = new Scanner(new BufferedReader(new FileReader("us_numbers.txt")))) {
s.useLocale(Locale.US);
while (s.hasNext()) {
if (s.hasNextDouble()) {
sum += s.nextDouble();
} else {
s.next();
}
}
System.out.println(sum);
}
}
}
/* 打印结果
1032778.74159
*/
格式化(Formatting)
实现了格式化的流对象,要么是字符流PrintWriter
的实例,要么是字节流PrintStream
的实例。
注意:你唯一会用到的PrintStream
对象是System.out
和System.err
,如果你需要格式化输出流,请实例化PrintWriter
,而不是PrintStream
。
和字节及字符流对象一样,PrintStream
和PrintWriter
实例为字节和字符输出实现了一套标准的写方法。除此之外,它们还实现了一套转换内部数据到格式化输出到方法。两类格式化方法是:
print
和println
用标准的方式格式化format
定制格式化
第一类比较简单,下面我们看看format
方法,它可以格式化多个参数。示例:
public class Root {
public static void main(String[] args) {
int i = 2;
double r = Math.sqrt(i);
System.out.format("The square root of %d is %f.%n", i, r);
}
}
/*
The square root of 2 is 1.414214.
*/
格式参数以%开头,1~2个字符的转化类型结束,常见的转化类型有:
d
格式化一个整数为定点数f
格式化一个浮点数为定点数n
输出当前平台的换行符x
格式化一个整数为16进制值s
格式化任何值为字符串tB
格式化一个整数为本地化的月份的名字
注意:
- 除了
%%
和%n
,所有的格式参数必须匹配一个参数。 - java的转义符
\n
总是生成一个固定的换行符(\u000A),通常不要使用\n
,要获得本地化正确的换行符,使用%n
除了类型转化,格式参数还可以包含一些元素来进一步定制输出格式,示例:
public class Format {
public static void main(String[] args) {
System.out.format("%f, %1$+020.10f %n", Math.PI);
}
}
/*
3.141593, +00000003.1415926536
*/
%1$+020.10f
格式说明:
%
格式参数开始1$
参数索引(个人理解,这里是取format方法索引为1的参数)。也可以使用<
匹配前一个格式的参数。比如这里的格式也可以写作:%f, %<+020.10f %n
+0
标识20
宽度.10
精度f
转化类型
命令行I/O
程序通常从命令行运行,并且通过命令行环境与用户交互。java对此提供了支持:通过标准流和通过控制台。
标准流(Standard Streams)
标准流是很多操作系统的特征,它们默认读取键盘的输入然后,输出到显示器。它们也支持文件及程序间的I/O,但是由命令行解释器控制的,而不是程序。
java支持三种标准流:
- Standard Input,通过
System.in
访问 - Standartd Output,通过
System.out
访问 - Standard Error,通过
System.err
访问
由于历史原因,这些都是字节流。System.out
和System.err
被定义为PrintStream
对象,它利用内部的字符流对象来模拟字符流的特性。System.in
没有字符流特性,但是可以用InputStreamReader
类包装一下来当作字符流使用:
InputStreamReader cin = new InputStreamReader(System.in);
控制台(Console)
Console
比标准流更高级,它的reader
和writer
方法提供了纯字符的输入输出流。另外,它对于安全地密码输入特别有用。
要使用Console
,需要先调用System.console()
方法获取一个Console
对象,如果返回NULL,要么是系统不支持,要么是程序不是在交互式环境启动的。
下面的示例展示如何使用控制台修改用户密码:
import java.io.Console;
import java.io.IOException;
import java.util.Arrays;
public class Password {
public static void main(String[] args) throws IOException {
// 获取控制台对象,如果不存在,则使用标准错误输出提示
Console c = System.console();
if (c == null) {
System.err.println("No console.");
System.exit(1);
}
String login = c.readLine("Enter your login: ");
char[] oldPassword = c.readPassword("Enter your old password: ");
if (verify(login, oldPassword)) {
boolean noMatch;
do {
char[] newPassword1 = c.readPassword("Enter your new password: ");
char[] newPassword2 = c.readPassword("Enter your password again: ");
noMatch = !Arrays.equals(newPassword1, newPassword2);
if (noMatch) {
c.format("password don't match. try again.%n");
} else {
change(login, newPassword1);
c.format("password for %s changed.%n", login);
}
// 用空格重置之前的输入
Arrays.fill(newPassword1, ' ');
Arrays.fill(newPassword2, ' ');
} while (noMatch);
}
Arrays.fill(oldPassword, ' ');
}
static boolean verify(String login, char[] password) {
// do something to verify
return true;
}
static void change(String login, char[] password) {
// do something to change password
}
}
这里使用readPassword
方法提示用户输入并读取输入,用户输入的内容不会显示。
数据流(Data Streams)
数据流支持所有基本数据类型及字符串的I/O。数据流要么实现了DataInput
接口,要么实现了DataOutput
接口,这些接口中使用的最多的实现是:
DataInputStream
DataOutputStream
下面的示例展示如何使用:
import java.io.*;
public class DataStreams {
static final String dataFile = "invoiceData.txt";
static final double[] prices = {19.99, 9.99, 15.99, 3.99, 4.99};
static final int[] units = {12, 8, 13, 29, 50};
static final String[] descs = {
"Java T-shirt",
"Java Mug",
"Duke Juggling Dolls",
"Java Pin",
"Java Key Chain"
};
public static void main(String[] args) throws IOException {
try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(dataFile)))) {
for (int i = 0; i < prices.length; i ++) {
out.writeDouble(prices[i]);
out.writeInt(units[i]);
out.writeUTF(descs[i]);
}
}
try(DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(dataFile)))) {
double price;
int unit;
String desc;
double total = 0.0;
try {
while (true) {
price = in.readDouble();
unit = in.readInt();
desc = in.readUTF();
System.out.format("you ordered %d units of %s at $%.2f%n",
unit, desc, price);
total += unit * price;
}
} catch (EOFException e) {}
System.out.format("For a total of : $%.2f%n", total);
}
}
}
注意:
- 数据流只能通过包装现成的字节流来创建
- 通过捕获
EOFException
异常,而不是判断无效的返回值,来检测文件的末尾。这是因为所有DataInput
接口的具体实现中的方法都使用EOFException
,而不是返回值。 - 另外这里用float类型来表示金额是不妥的,应该使用
java.math.BigDecimal
,但后者是对象类型,与数据流不兼容,需要使用对象流。
对象流(Object Streams)
对象流支持对象I/O。大部分标准类支持其对象的序列化,也就是实现了Serializable
接口。
对象流有两个类:
ObjectInputStream
ObjectOutputStream
这两个类实现了ObjectInput
和ObjectOutput
接口,而这两个接口分别是DataInput
和DataOutput
的子接口。这意味着数据流中所有基本数据类型的I/O方法在对象流中也实现了。所以对象流中既可以包含基本数据的值,也可以包含对象的值。
上面数据流的例子完全可以用对象流来重写:
import java.io.*;
import java.math.BigDecimal;
import java.util.Calendar;
public class ObjectStreams {
static final String dataFile = "invoiceData2";
static final BigDecimal[] prices = {
new BigDecimal("19.99"),
new BigDecimal("9.99"),
new BigDecimal("15.99"),
new BigDecimal("3.99"),
new BigDecimal("4.99")};
static final int[] units = {12, 8, 13, 29, 50};
static final String[] descs = {"Java T-shirt",
"Java Mug",
"Duke Juggling Dolls",
"Java Pin",
"Java Key Chain"};
public static void main(String[] args)
throws IOException, ClassNotFoundException {
try (ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dataFile)))) {
out.writeObject(Calendar.getInstance());
for (int i = 0; i < prices.length; i ++) {
out.writeObject(prices[i]);
out.writeInt(units[i]);
out.writeUTF(descs[i]);
}
}
try (ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(dataFile)))) {
Calendar date = null;
BigDecimal price;
int unit;
String desc;
BigDecimal total = new BigDecimal(0);
date = (Calendar) in.readObject();
System.out.format("On %tA, %<tB %<te, %<tY:%n", date);
try {
while (true) {
// 如果readObject没有返回预期的对象,强制转化会抛出ClassNotFoundException异常
price = (BigDecimal) in.readObject();
unit = in.readInt();
desc = in.readUTF();
System.out.format("You ordered %d units of %s at $%.2f%n",
unit, desc, price);
total = total.add(price.multiply(new BigDecimal(unit)));
}
} catch (EOFException e) {}
System.out.format("For a TOTAL of: $%.2f%n", total);
}
}
}
可以看出,对象流完全兼容数据流中的操作。
复杂对象的输出和输入
对象流的writeObject
和readObject
方法使用简单,但是内部包含非常牛逼的对象管理逻辑。readObject
从流中复原对象时,它也要复原该对象的所有引用,而这些引用的对象可能还有其他的引用。writeObject
方法会遍历对象的所有引用关系,并将它们写入流中。
如果一个流中的两个对象都引用了同一个对象,读回时,它们还是会引用同一个对象。一个流只能包含一个对象的一份拷贝,但是可以包含对该对象的任意数量的引用。因此,如果你将一个对象写入流中两次,你只是写入了两次引用。例如:
Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);
一个writeObject
必须匹配一个readObject
,因此读回对象时:
Object ob1 = in.readObject();
Object ob2 = in.readObject();
拿到了两个对象,ob1和ob2,但都是对ob对象的引用。