Java I/O系统
1、流家族图谱
2、InputStream/OutputStream
2.1 FileInputStream/FileOutputStream
基本的字节流
public class Main {
public static void main(String[] args) {
try(FileInputStream in = new FileInputStream("in.txt");
FileOutputStream out = new FileOutputStream("out.txt")) {
int len = 0;
byte[] bytes = new byte[1024];
while((len = in.read(bytes)) != -1) {
out.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.2 FilterInputStream/FilterOutputStream
① BufferedInputStream/BufferedOutputStream
对流使用缓冲区技术,每次向流读取/写入时,不必每次都进行实际的物理读取/写入操作
public class Main {
public static void main(String[] args) {
try(BufferedInputStream in = new BufferedInputStream(new FileInputStream("in.txt"));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("out.txt"))) {
...
} catch (IOException e) {
e.printStackTrace();
}
}
}
② DataInputStream/DataOutputStream
允许从流读取/写入基本数据类型及字符串(跨平台)
public class Main {
public static void main(String[] args) {
try(DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("data.txt")))) {
out.writeUTF("PI");
out.writeDouble(3.14159);
} catch (IOException e) {
e.printStackTrace();
}
try(DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.txt")))) {
System.out.println(in.readUTF());
System.out.println(in.readDouble());
} catch (IOException e) {
e.printStackTrace();
}
}
}
③ PrintStream
继承自 FilterOutputStream,用于格式化输出
public class Main {
public static void main(String[] args) {
try (PrintStream out = new PrintStream("out.txt")) {
out.println("Hello World!");
out.printf("%s %s!", "Hello", "World");
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.3 ByteArrayInputStream/ByteArrayOutputStream
对字节数组进行读取/写入,该流不用关闭
public class Main {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write("Hello World!".getBytes());
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
int c = 0;
while((c = in.read()) != -1) {
System.out.print((char)c);
}
}
}
2.4 ObjectInputStream/ObjectOutputStream
请看 7.1 节
3、Reader/Writer
在使用字符流时,注意保证字符的编码和解码
方式的一致性。
3.1 InputStreamReader/OutputStreamWriter
① FileReader/FileWriter
基本的字符流,是 InputStreamReader/OutputStreamWriter 的快捷方式(用流构造对象时,不能使用此快捷方式)
public class Main {
public static void main(String[] args) {
try(FileReader in = new FileReader("in.txt");
FileWriter out = new FileWriter("out.txt");) {
int len = 0;
char[] chars = new char[1024];
while((len = in.read(chars)) != -1) {
out.write(chars, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.2 BufferedReader/BufferedWriter
对流使用缓冲区技术,每次向流读取/写入时,不必每次都进行实际的物理读取/写入操作(有readLine
方法)
public class Main {
public static void main(String[] args) {
try(BufferedReader in = new BufferedReader(new FileReader("in.txt"));
BufferedWriter out = new BufferedWriter(new FileWriter("out.txt"))) {
...
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3 PrintWriter
BufferedWriter 的快捷方式,并且包含格式化的方法
public class Main {
public static void main(String[] args) {
try (PrintWriter out = new PrintWriter("out.txt")) {
...
} catch (IOException e) {
e.printStackTrace();
}
}
}
4、RandomAccessFile
类似于组合使用了 DataInputStream/DataOutputStream,并且可以利用seek()
在文件中进行随机访问
public class Main {
static String file = "data.txt";
static void display() throws IOException {
RandomAccessFile rf = new RandomAccessFile(file, "r");
for (int i = 0; i < 7; i++) {
System.out.printf("Value %d: %f\n", i, rf.readDouble());
}
System.out.println(rf.readUTF());
rf.close();
}
public static void main(String[] args) {
try (RandomAccessFile rf = new RandomAccessFile(file, "rw")) {
for (int i = 0; i < 7; i++) {
rf.writeDouble(i * 1.414);
}
rf.writeUTF("The end of the file");
display();
} catch (IOException e) {
e.printStackTrace();
}
try (RandomAccessFile rf = new RandomAccessFile(file, "rw")) {
// double占用8字节,5*8即第5个double的结尾
rf.seek(5 * 8);
rf.writeDouble(777.777);
display();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5、标准 I/O
System.in 和 System.err 类型为 PrintStream,可以直接使用;System.in 类型为 InputStream,对其包装后才能使用(源自 Java 编程思想 P548,待议:没有包装好像也可以使用)。
① 从标准输入中读取:
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line;
while((line = in.readLine()) != null && line.length() != 0) {
System.out.println(line);
}
}
}
② 将标准输出转换为 PrintWriter:
public class Main {
public static void main(String[] args) {
PrintWriter out = new PrintWriter(System.out, true);
out.println("Hello World!");
}
}
③ 标准 I/O 重定向:可以使用 System 中的 setIn、setOut 和 setErr,对标准 I/O 进行重定向。
6、新 I/O(NIO)
To Be Continued~
7、对象序列化
Java 的对象序列化
将那些实现了Serializable
接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。
7.1 保存和加载序列化对象
使用 ObjectInputStream 的writeObject
方法将对象序列化,ObjectOutputStream 的readObject
方法将对象反序列化。注意,对象所属的类需要实现 Serializable 接口。
class Employee implements Serializable {
private String name;
private double salary;
public Employee() {}
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
'}';
}
}
public class Main {
public static void main(String[] args) {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.txt"))) {
out.writeObject("Hello World!");
out.writeObject(new Employee("张三", 1800.0));
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.txt"))) {
String s = (String) in.readObject();;
Employee e = (Employee) in.readObject();
System.out.println(s + "\n" + e);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
反序列化时,要保证序列化的对象所属的类在类路径
中,否则在类型转换时会抛出 ClassNotFoundException 异常。
7.2 修改默认的序列化机制
① Externalizable 接口
Externalizable
接口继承了 Serializable 接口,实现该接口可以对序列化的过程进行控制。
需要注意,Externalizable 对象的序列化和反序列化与 Serializable 不同:
- Serializable:将对象完全序列化,并且反序列化时不会调用构造器
- Externalizable:序列化时调用 writeExternal 方法序列化字段,反序列化时首先调用
公共的
无参构造器,然后调用 readExternal 方法反序列化字段
示例代码:
class Employee implements Externalizable {
...
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
// out.writeObject(salary);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String)in.readObject();
// salary = (Double)in.readObject();
}
}
public class Main {
public static void main(String[] args) {
...
}
}
如上所示,序列化时对象时不会序列化 salary 字段。
② transient(瞬时)关键字
如果想对 Serializable 对象进行序列化的控制,可以使用transient
关键字逐字段的关闭序列化:
private transient double salary;
③ 添加 writeObject 和 readObject 方法
如果不想实现 Externalizable 接口,我们还可以实现 Serializable 接口,并添加(并非覆盖或实现)名为writeObject
和readObject
的方法,在序列化或反序列化时就会分别调用这两个方法。
注意,这两个方法必须按照如下格式定义:
private void writeObject(ObjectOutputStream stream) throws IOException {...}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {...}
如果想在 writeObject/readObject 方法中使用默认的序列化或反序列化机制,可以在其中调用defaultWriteObject/defaultReadObject
方法。
7.3 序列化单例存在的问题
示例代码:
class Demo implements Serializable {
public static final Demo DEMO;
static { DEMO = new Demo(); }
private Demo() {}
}
public class Main {
public static void main(String[] args) {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bout)) {
out.writeObject(Demo.DEMO);
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()))) {
Demo demo = (Demo) in.readObject();
System.out.println(demo == Demo.DEMO);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
false
可以发现,Demo 是单例的,但是反序列化得到了一个新的实例对象!
解决方案:添加readResolve
方法,它会在反序列化时被调用。
class Demo implements Serializable {
...
protected Object readResolve() {
return Demo.DEMO;
}
}
还可以在
7.4 序列号和序列化版本号
①序列号
:序列化时,每个对象都被赋予一个序列号,相同对象的出现将被存储为对这个对象序列号的引用。
示例代码:
class Employee implements Serializable {...}
public class Main {
public static void main(String[] args) {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.txt"))) {
Employee e = new Employee("张三", 1800.0);
out.writeObject(e);
out.writeObject(e);
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.txt"))) {
Employee e1 = (Employee) in.readObject();
Employee e2 = (Employee) in.readObject();
System.out.println(e1 == e2);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
true
可以发现,如果将一个对象序列化两次,那么反序列化将得到两个相同的对象。
②序列化版本号
:当序列化对象时,对象所属的类也会被存储,其中就包含了序列化版本号,它是字段类型和方法签名的指纹(SHA 计算结果的前 8 个字节)。
当反序列化一个对象时,会拿其指纹与它所属类的当前指纹进行比较,如果不匹配就说明这个类的定义在写出该对象后发生了改变,这将会产生一个异常。
示例代码:
class Employee implements Serializable {
...
public void foo() {}
}
运行结果:
java.io.InvalidClassException: com.company.Employee; local class incompatible: stream classdesc serialVersionUID = 5547933960923146683, local class serialVersionUID = -1727657969676927935
如上所示,我们为 Employee 添加一个方法,然后再进行反序列化,得到 InvalidClassException 异常。根据错误信息可知,这是由序列化版本号不匹配引发的异常。
这种情况下,如果我们想要反序列化成功,就需要在类的定义中手动添加旧版本类的指纹,以此表明它对旧版本兼容:
class Employee implements Serializable {
...
public static final long serialVersionUID = 5547933960923146683L;
public void foo() {}
}
8、操作文件
To Be Continued~
9、补充内容
9.1 Scanner
public class Main {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
while(scanner.hasNext()) {
String line = scanner.nextLine();
System.out.println(line);
}
}
}
}
9.2 性能比较(待完善)
测试复制 1GB 文件所需的时间:
方法 | 时间 |
---|---|
FileInputStream/FileOutputStream | 9 秒 |
BufferedInputStream/BufferedOutputStream | 5.5 秒 |
RandomAccessFile | 15.8 秒 |
使用 FileChannel 的 transferTo/transferFrom 方法 | 3.8 秒 |
如有错误,欢迎指正。.... .- ...- . .- -. .. -.-. . -.. .- -.-- -.-.--