本篇文章主讲 Java IO,使用的 Java 版本为 Java 8,先说下结论:
字节流:使用字节进行输入输出。其他流都是基于这个流。Java 官方强调这个不推荐日常使用。
字符流:我们平常输入输出几乎都是字符,使用字节流一个一个字节读取就不合适了,于是出现了字符流,字符流包装了字节流,它是操作字符的。
缓冲流:字节流和字符流,都是每一次进行读写就会进行一个物理 IO 操作,效率不高。于是出现缓冲流,写操作先写进内存中的一个区域(缓冲区),写满在调用物理 IO。读操作也是先读取缓冲区,读满再展示。
Scanning 和 Formatting:平时读取和写入是需要一些格式的,比如像读取不同数据类型的数据、换行输入内容。这时就用到 Scanning 和 Formatting。Scanning 的代表是 Scanner 类,虽然它不是流,但是它包装了流。Formatting 最常用的就是我们的 System.out,它实际上是 PrintStream 对象。
命令行 I/O:标准流和 Console。用于命令行上的读写。标准流有三种:System.in、System.out、System.err。Console,必须要在命令行交互的情况下才能使用,它相比较于标准流,可以安全的读取重要敏感数据(比如密码)。
Data Streams:用于处理二进制 I/O 基本数据类型和 String 的读写。它们是包装了字节流,更方便我们操作基本数据类型和 String 的读写。
Object Streams:用于处理二进制对象的读写。它们也可以处理基本类型和 String,因为它们共同直接或间接实现了同样的接口 DataInput、DataOutput。拥有同样的功能。
字节流
使用字节进行输入输出,所有的字节流类都源于 InputStream、OutputStream。
以 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("xanadu.txt");
out = new FileOutputStream("outagain.txt");
int d;
while ((d = in.read()) != -1) {
out.write(d);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
读取图解
字节输入流读取数据 read,读取的数据赋值到 d,将 d 写入输出流。
注意事项
当流不再被使用时,一定要关闭。可以看到程序中是在 finally 关闭字节流的(close 方法)。当出现异常时,in、out 可能为 null,所以关闭前进行了判空。
字节流的使用场景
字节流应该是被避免使用的一种低级 IO(low level I/O)。当 xanadu.txt 包含字符数据时,最好使用字符流。
So why talk about byte streams? Because all other stream types are built on byte streams.
那为什么还要学字节流,因为所有其他流类型都基于字节流。
字符流
在大多数应用中,字符流都可以替代字节流。
字符流使用
所有的字节流类都源于 Reader 和 Writer。
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CopyCharacters {
public static void main(String[] args) throws IOException {
FileReader inputStream = null;
FileWriter outputStream = null;
try {
inputStream = new FileReader("xanadu.txt");
outputStream = new FileWriter("characteroutput.txt");
// 这里 int 存储的是一个字符值(使用 int 的后 16 位)
int c;
while ((c = inputStream.read()) != -1) {
outputStream.write(c);
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
}
这个程序和字节流的很像,区别在于:FileReader 被替换成 FileInputStream,FileWriter 被替换成 FileOutputStream。
字符流和字节流
• 字节流源于:InputStream、OutputStream
• 字符流源于:Reader、Writer。
字符流是字节流的包装。字符流使用字节流来进行物理 I/O,并且处理字符和字节的转换。
Java 官方不推荐使用字节流,只要数据包含字符,就应该使用字符流。
Scanning and Formatting
在使用输入输出时,一般会喜欢格式化。Java 提供两种 API 来帮助实现。scanner 和 format,分别用来输入和输出不同格式的数据。
Scanning
代表类就是 Scanner 类,使用例子如下:
public class ScanSum {
public static void main(String[] args) throws IOException {
Scanner s = null;
double sum = 0;
try {
s = new Scanner(new BufferedReader(new FileReader("usnumbers.txt")));
// 设置语言环境(不同语言环境千分位可能不一样)
s.useLocale(Locale.CHINA);
// Scanner 对象可以调用 nextXXX 方法来读取不同数据类型的数据。
while (s.hasNext()) {
if (s.hasNextDouble()) {
sum += s.nextDouble();
} else {
s.next();
}
}
} finally {
if (s != null) {
s.close();
}
}
// System.out 是 PrintStream 对象
System.out.println(sum);
}
}
Scanner 对象不是流,但是包装了输入流,所以可以进行 I/O 操作。通过它的对象调用 nextXXX 方法,就可以读取不同类型的值。
Formatting
我们常用的 System.out 其实就是 PrintStream 的对象。与它类似的还有 System.err。在需要自定义对象时,要使用类 PrintWriter 而不是 PrintStream。
常用的方法是:print()、println()、format()。
public class FormatDemo {
public static void main(String[] args) {
int i = 2;
double r = Math.sqrt(i);
// 换行使用 %n 而不是 \n(\n 会生成一个换行符)
System.out.format("The square root of %d is %f.%n", i, r);
// 格式化日期,输出月份
System.out.format("%tB", new Date());
}
}
格式说明符:
最常用的格式说明符:
d:整数
f:浮点数
s:字符串
%n:用来换行,需要换行时不推荐使用 \n,使用 %n,Java 会根据操作系统生成不同的换行符。
总结
当输入与输出的都是不同类型、不同格式的数据时,就可以用 Scanning 和 Formatting。
Scanning:代表类 Scanner。它不是流,但是因为包装了输入流,所以可以进行 IO 操作。
Formatting:最常被使用的就是 System.out 。他是 PrintStream 的对象,在需要自定义 Formatting 类型的对象时,要使用 PrintWriter
创建对象而不是 PrintStream。
命令行 IO
从命令行读写有两种方式:
- 通过标准流(Standard Streams)
- 通过控制台(Console)
Standard Streams
标准流,一般来说是从键盘读取,在控制台显示读取的内容。Java 有三种标准流:
- System.in:标准输入
- System.out:标准输出
- System.err:标准错误
System.in 是字节流不是字符流,如果想要使用字符标准输入,需要使用 InputStreamReader 包装(转为字符流):
public class StandardStreamDemo {
public static void main(String[] args) throws IOException {
int b = 0;
InputStreamReader in = new InputStreamReader(System.in);
try {
while (((b = in.read()) != -1))
System.out.println((char) b);
} finally {
in.close();
}
}
}
其实 Scanner 本身就做了这样的操作,它的其中一个构造方法如下:
public Scanner(InputStream source) {
this(new InputStreamReader(source), WHITESPACE_PATTERN);
}
Console
相比较于 Standard Streams,他更安全,可以用来安全的输入密码(readPassword 方法)。
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 new password again: ");
noMatch = !Arrays.equals(newPassword1, newPassword2);
if (noMatch) {
c.format("Passwords 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, ' ');
}
// Dummy change method.
static boolean verify(String login, char[] password) {
// This method always returns
// true in this example.
// Modify this method to verify
// password according to your rules.
return true;
}
// Dummy change method.
static void change(String login, char[] password) {
// Modify this method to change
// password according to your rules.
}
}
上面的程序步骤:
- 拿到 Console 对象。(System.console())(必须在命令行下执行 Java 程序,如果用 IDE 会拿不到 Console 对象)
- 通过 readLine 拿到登录用户
- 通过 readPassword 拿到旧密码(使用该方法命令行不会显示输入的内容)
- 验证(此处为假逻辑)
- 通过 readPassword 拿到新密码和确认密码
- 修改密码(此处为假逻辑)
- 旧密码已被覆盖
程序的效果大概是这样的:
总结
命令行 I/O 在 Java 有两种实现:
- Stardard Streams
- Console
其中 Stardard Streams 有三种:
System.in:标准输入
System.out:标准输出
System.err:错误输出
而 Console 相比较与 Stardard Streams,可以安全的,在命令行获取输入的密码(不会显示),但是必须是在命令行才可以获取 Console 对象。
Data Streams
Data Streams 支持二进制 I/O(八大基本数据类型和 String)。它们的实现类都实现接口 DataInput、DataOutPut。这次的例子使用的是它们最广泛的实现类 DataInputStream、DataOutPutStream。
import java.io.*;
public class DataStreamDemo {
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 {
// DataOutputStream 包装已有 buffer 字节输出流对象
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]);
// 将 descs[i]以 UTF-8 编码的变化形式,写入文件
out.writeUTF(descs[i]);
}
// 刷新缓冲区
out.flush();
// 读取文件
// DataInputStream 包装已有字节流对象(包装的文件输入流)
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) {
// 用异常来终止 while 循环(读取文件结束继续读取会抛出 EOFException 异常)
}
}
}
总结
Data Streams,是 Java I/O 提供的,给基本类型和 String 的二进制输入输出流。
数据流类都是实现的 DataInput 和 DataOutPut 接口。
上面只讲了最常用的 DataInputStream 和 DataOutputStream。它们都是包装已有的字节流对象。
Object Streams
object streams 支持 Object I/O,但是前提是对象所属的类已经实现 Serializable
接口。
Object Streams 类是:ObjectInputStream、ObjectOutputStream。
ObjectInputStream 体系图:
可以看到它拥有 DataInput、ObjectInput 接口的的所有功能,所以 Data Streams 的例子,对于 Object Streams 仍然适用:
Object Streams 处理基本数据类型与 String 类型
DataOutputStream 换成 ObjectOutputStream :
ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(
new FileOutputStream(dataFile)));
DataInputStream 换成 ObjectIntputStream :
ObjectInputStream in = new ObjectInputStream(new
BufferedInputStream(new FileInputStream(dataFile)));
运行后结果与使用 Data Streams 一致。
完整代码:
import java.io.*;
public class ObjectStreamDemo {
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 {
// DataOutputStream 包装已有 buffer 字节输出流对象
ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(
new FileOutputStream(dataFile)));
// 写数据到文件
for (int i = 0; i < prices.length; i ++) {
out.writeDouble(prices[i]);
out.writeInt(units[i]);
// 将 descs[i]以 UTF-8 编码的变化形式,写入文件
out.writeUTF(descs[i]);
}
// 刷新缓冲区
out.flush();
// 读取文件
// ObjectInputStream 包装已有字节流对象(包装的文件输入流)
ObjectInputStream in = new ObjectInputStream(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) {
// 用异常来终止 while 循环(读取文件结束继续读取会抛出 EOFException 异常)
}
}
}
Object Streams 处理复杂类型
通过 readObject 和 writeObject 处理复杂类型。在写入对象时,会读取对象及对象的引用对象,如图所示, a 包含 b 和 c,b 包含 d 和 e,在写入时会将这些都写入。在读取时也会将这些对象都读出来。
实体类 Student:
public class Student implements Serializable {
private String name;
public Student(String name) {
this.name = name;
}
}
要进行读写的对象所属类一定要实现 Serializable ,否则无法使用 readObject 和 writeObject 方法。
使用示例:
public class ObjectStreamDemo2 {
static final String dataFile = "invoicedata.txt";
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(
new FileOutputStream(dataFile)));
Student stu = new Student("zhangsan");
out.writeObject(stu);
out.writeObject(stu);
// 刷新缓冲区
out.flush();
// 读取文件
ObjectInputStream in = new ObjectInputStream(new
BufferedInputStream(new FileInputStream(dataFile)));
Object stu1 = in.readObject();
Object stu2 = in.readObject();
// true
System.out.println(stu1.equals(stu2));
}
}
我们创建 Student 的对象 stu 写入文件两次,读取后,取出对象发现它们地址相同(是同一个对象)。
A stream can only contain one copy of an object, though it can contain any number of references to it.
上面的大概意思是:虽然一个流可以包含多个引用,但是它只是对对象做了拷贝。
我们从一个流写入两次,读取两次相同对象时,发现对象地址是一致的。
总结
Object Streams 可以处理实现 Serializable
接口的实现类的对象的读写。
它实现 ObjectInput 接口,而 ObjectInput 接口是 DataInput 接口的子接口,所以它也可以处理基本数据类型和 String 的读写。
在进行读写时,对象包含的引用对象也会一起进行读写。
Java IO 小结
Java IO ,有字节流、字符流、缓冲流、命令行 IO、Data Streams、Object Streams。他们实际上最终都是用字节流来调用物理 IO 进行读写操作。其他流是为了让我们更加方便、有效率的进行 IO 操作。