IO(二):Reader,Writer和PrintWriter
1.读取classpath资源
我们知道,Java存放.class的目录或jar包也可以包含任意其他类型的文件,例如:
配置文件,例如.properties;
图片文件,例如.jpg;
文本文件,例如.txt,.csv;
……
从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties文件放到classpath中,就不用关心它的实际存放路径。
在classpath中的资源文件,路径总是以/开头,我们先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件:
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
// TODO:
}
如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:
Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile("./conf.properties"));
这样读取配置文件,应用程序启动就更加灵活。
2.序列化
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:
public interface Serializable {
}
2.1.序列化
把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流:
public static void main(String[] args) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try(ObjectOutputStream outputStream = new ObjectOutputStream(stream)){
outputStream.write(123);
outputStream.writeUTF("Hello");
outputStream.writeObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(stream.toByteArray()));
}
ObjectOutputStream既可以写入基本类型,如int,boolean,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object。
因为写入Object时需要大量的类型信息,所以写入的内容很大。
2.2.反序列化
和ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。
readObject()可能抛出的异常有:
ClassNotFoundException:没有找到对应的Class;
InvalidClassException:Class不匹配。
实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。
3.Reader
Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取:
reader.jpg
3.1.FileReader
FileReader是Reader的一个子类,它可以打开文件并获取Reader。下面的代码演示了如何完整地读取一个FileReader的所有字符:
public static void main(String[] args) throws IOException {
Reader reader = new FileReader("C:\\upload_file\\IOTest.txt");
while(true){
int n = reader.read();
if(n == -1){
break;
}
System.out.println((char) n);
}
reader.close();
}
我们可以先设置一个缓冲区,然后,每次尽可能地填充缓冲区:
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt")) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}
3.2.InputStreamReader
Reader和InputStream有什么关系?
除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream。
既然Reader本质上是一个基于InputStream的byte到char的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。示例代码如下:
// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");
使用InputStreamReader,可以把一个InputStream转换成一个Reader。
4.Writer
Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
Writer和OutputStream的区别如下:
writer.jpg
Writer是所有字符输出流的超类,它提供的方法主要有:
写入一个字符(0~65535):void write(int c);
写入字符数组的所有字符:void write(char[] c);
写入String表示的所有字符:void write(String s)。
4.1.FileWriter
FileWriter就是向文件中写入字符流的Writer。它的使用方法和FileReader类似:
try (Writer writer = new FileWriter("readme.txt")) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}
4.2.OutputStreamWriter
普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
// TODO:
}
上述代码实际上就是FileWriter的一种实现方式。这和上一节的InputStreamReader是一样的。
5.PrintStream和PrintWriter
PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:
写入int:print(int)
写入boolean:print(boolean)
写入String:print(String)
写入Object:print(Object),实际上相当于print(object.toString())
…
以及对应的一组println()方法,它会自动加上换行符。
PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException。
5.1.PrintWriter
PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。两者的使用方法几乎是一模一样的:
public static void main(String[] args) throws IOException {
StringWriter writer = new StringWriter();
try(PrintWriter printWriter = new PrintWriter(writer)){
printWriter.println("hello");
printWriter.println(123);
printWriter.println(true);
}
System.out.println(writer.toString());
}