InputStream与OutputStream
InputStream与OutputStream初探
InputStream与OutputStream可以将一切输入来源与输出目的地进行抽象化,就算不知道具体的输入来源和输出目的地,我们也可以进行数据的读取与写入,这就是这两个API的主要功能。
代码实例:
import java.io.*;
public class IO{
public static void dump(InputStream src, OutputStream dest) throws IOException{ //抽象化数据输入来源与目的地
try(InputStream input = src, OutputStream output = dest){ //尝试自动关闭资源
byte[] data = new byte[1024];
int length;
while((length = input.read(data)) != -1){
output.write(data, 0, length);
}
}
}
}
我们可以看到,在这个实例中,我们事先并不知道输入来源是什么,有可能是文档,有可能是数据库,还有可能是网络输入输出。但是就算我们不知道要读取的内容是什么,并且不知道要将数据以怎样的格式存储起来,我们还是将输入与输出直接抽象化,这样不管是什么样的数据我们都可以读取,存储。
如果我们要将某个文档读入并存入另一个文档之中,那么我们可以这样使用:
import java.io.*;
public class Copy {
public static void main(String[] args) throws IOException{
IO.dump(
new FileInputStream(args[0]), //文件输入流
new FileOutputStream(args[1]) //文件输出流
);
}
}
这个程序可以由命令行自变量指定读取的文档来源与输出目的地:
这里写代码片QkFCMA==/dissolve/70/gravity/SouthEast” alt=”这里写图片描述” title=”” />
结果:
我们可以看到,已经将creattree中的内容写入到了1.txt中了。
我们在后面将会介绍到FileInputStream是InputSream的子类,用于衔接文档以读入数据;FileOutputStream是OutputStream的子类,用于衔接文档以写出数据。
我们还可以从HTTP服务器读取某个网页,并另存为文档,代码如下:
import java.io.*;
import java.net.URL;
public class Download {
public static void main(String[] args) throws IOException{
URL url = new URL(args[0]);
InputStream src = url.openStream(); //数据来源
OutputStream dest = new FileOutputStream(args[1]); //数据目的地
IO.dump(src, dest);
}
}
对于这个代码不做过多的解释。
我们可以看到,无论来源或目的地的实体形式如何,只要想办法取得InputStream和OutputStream,那么接下来都是调用这两个的相关方法。
串流继承架构
前面说过,要了解一个API,活用一个API,我们必须了解他的继承架构,这样我们才会知道在什么情况下改用哪些API。
上面的图片来自百度,我们可以清晰的看到InputStream的继承架构,我们来详细看看每个子类都有什么含义。
标准输入/输出
System.in与System.out分别是InputStream和PrintStream的实例,分别代表标准输入和标准输出,对于个人计算机而言,通常对应文本模式中的输入与输出。因为我们在文本模式下一般是取得整行用户输入,因此较少直接操作InputStream,而是用Scanner打包System.in。
我们可以使用setIn的方法指定InputStream实例,重新指定标准输入来源,下面我们将标准输入来源指定为FileInputStream,可以读取指定文档并显示在文本模式之中。
import java.io.*;
import java.util.*;
public class StandardIn {
public static void main(String[] args) throws IOException{
System.setIn(new FileInputStream(args[0])); //改变标准输入来源
try(Scanner scanner = new Scanner(System.in)){ //已经将文档输入变为标准输入
while(scanner.hasNextLine()){
System.out.println(scanner.nextLine());
}
}
}
}
标准输出也可以重新导至文档,在执行程序的时候用>就可以,和Linux下的重定向还有点相似。
可以使用System的setOut()方法指定PrintStream实例,将结果输出至指定的目的地。
import java.io.*;
public class StandardOut {
public static void main(String[] args) throws IOException{
try(PrintStream printStream = new PrintStream( //指定输出流
new FileOutputStream(args[0]))){
System.setOut(printStream); //改变标准输出流
System.out.println("HelloWorld");
}
}
}
除了前两个之外,还有一个System.err为Printstream实例,成为标准错误输出串流,它是用来立即显示错误信息的。例如在文本模式之下,System.out输出的信息可以使用>或>>重新导向至文档,但System.err的输出信息一定会显示在文本模式之中,无法进行重新导向,也可以使用System.setErr()指定Printstream,重新指定标准的错误输出串流。
FileInputStream与FileOutputStream
FileInputStream与FileOutputstream分别是InputStream和OutputStream的子类,可以指定文件名创建实例,一旦创建文档就开启,然后就可以进行数据的读写,这两个类在不使用的时候都要使用close()关闭文档。
FileInputStream主要操作了InputStream的read方法,FileOutputstream主要操作了OutputStream的write方法。
FileInputstream和FileOutputstream在进行文件的读写时,都是以字节为单位的,通常会使用高阶类进行打包,进行一些高阶操作,之后我们会介绍打包器类。
ByteArrayInputStream与ByteArrayOutputStream
ByteArrayInputStream与ByteArrayOutputStream可以指定byte数组创建实例,一旦创建就可以将byte数组当做数据源/目的地进行读写。
ByteArrayInputStream与ByteArrayOutputStream分别主要操作了InputStream和OutputStream的read方法和write方法。
串流处理装饰器(打包器)
InputStream和OutputStream提供最基本的串流操作,如果想要对输入/输出的数据进行加工处理,则可以使用打包器类也称装饰器,例如Scanner类就是打包器,它会操作打包的InputStream取得数据,并转换为你想要的数据类型。
PrintStream也是打包器类,他会自动转换为byte字节数组,利用打包的OutputStream进行输出。
常用的打包器类有具有缓冲区作用的BufferedInputStream和BufferedOutputStream,具备数据转换处理作用的DataInputStream和DataOutputStream,具备对象串行化能力的ObjectInputStream和ObjectOutputStream等。
BufferedInputStream与BufferedOutputStream
我们以文档存取为例,如果是FileInputStream和FileOutputStream实例,那么每次read()时都会要求读取硬盘,每次write时都会要求写入硬盘,这就会花费很多时间在硬盘的定位上,如果我们能够一次性读取足够多的数据至缓冲区,然后我们每次read都在缓冲区中取,那么我们就可以减少从硬盘读取的次数,对读取效率将会有帮助,如果我们可以在写入的时候都先将数据写入内存的缓冲区中,缓冲区满了在将缓冲区中的数据写入目的地,那么我们就可以减少对目的地的写入次数,对写入效率也会有一些帮助。我们可以使用默认或者自定义的缓冲区大小。具体实现如下:
import java.io.*;
public class BufferedIO {
public static void dump(InputStream src, OutputStream dest) throws IOException{
try(InputStream input = new BufferedInputStream(src);
OutputStream output = new BufferedOutputStream(dest)){
byte[] data = new byte[1024];
int length;
while((length = input.read(data)) != -1){
output.write(data, 0, length);
}
}
}
}
从上面的代码我们可以看到,创建BufferedInputStream和BufferedOutputStream实例必须将InputStream和OutputStream进行打包。
DataInputStream与DataOutputStream
这两个类提供读取写入Java基本数据类型的方法,像是读写int,double,boolean等方法。这些方法会自动的在指定的类型与字节间进行转换,不用你亲自做字节与类型的准换。来看一个实际的例子。下面的Member类可以调用save()存储Member实例本身的数据,文件名为Member的会员号码,调用Member.load()指定会员号码,则可以读取文档中的会员数据,封装为Member实例并返回。
import java.io.*;
public class Member {
private String number;
private String name;
private int age;
public Member(String number, String name, int age){ //构造函数
this.number = number;
this.name = name;
this.age = age;
}
@Override
public String toString(){
return String.format("(%s %s %d)", number, name, age);
}
public void save() throws IOException{
try(DataOutputStream output = new DataOutputStream(new FileOutputStream(number))){
output.writeUTF(number);
output.writeUTF(name);
output.writeInt(age);
}
}
public static Member load(String number) throws IOException{
Member member;
try(DataInputStream input = new DataInputStream(new FileInputStream(number))){
member = new Member(input.readUTF(), input.readUTF(), input.readInt());
}
return member;
}
}
下面是个使用Member类的例子:
import java.io.IOException;
import static java.lang.System.out;
public class MemberDemo {
public static void main(String[] args) throws IOException{
Member[] members = {
new Member("B1234", "Justin", 90),
new Member("B5678", "Monica", 95),
new Member("B9876", "Irene", 88),
};
for(Member member : members){
member.save();
}
out.println(Member.load("B1234"));
out.println(Member.load("B5678"));
out.println(Member.load("B9876"));
}
}
ObjectInputStream与ObjectOutputStream
前面的实例是将Member的number,name,age数据进行存储,读回时也是先取得number,name,age数据再创建Member实例。实际上我们可以使用这两个类将内存中的对象整个存储下来,之后再还原为对象。
使用这两个类我们就可以使用readObject方法将数据读入为对象,而writeObject方法将对象写至目的地,可以被这两个方法处理的对象,必须操作java.io.Serializable接口,这个接口中没有任何方法,只是做标示用的,标示这个对象是可串行化的。
来看一下前一个范例的改写。
import java.io.*;
public class Member2 implements Serializable{ //操作Serializable
private String number;
private String name;
private int age;
public Member2(String number, String name, int age){
this.number = number;
this.name = name;
this.age = age;
}
@Override
public String toString(){
return String.format("(%s %s %d)", number, name, age);
}
public void save() throws IOException{
try(ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(number))){
output.writeObject(this);
}
}
public static Member2 load(String number) throws IOException, ClassNotFoundException{
Member2 member;
try(ObjectInputStream input = new ObjectInputStream(new FileInputStream(number))){
member = (Member2) input.readObject();
}
return member;
}
}
下面的代码用来测试上面的类是否正确:
import java.io.*;
import static java.lang.System.out;
public class Member2Demo {
public static void main(String args[]) throws Exception{
Member2[] member2s = {
new Member2("B1234", "Justin", 90),
new Member2("B5678", "Monica", 95),
new Member2("B9876", "Irene", 88),
};
for(Member2 member2 : member2s){
member2.save();
}
out.println(Member2.load("B1234"));
out.println(Member2.load("B5678"));
out.println(Member2.load("B9876"));
}
}
字符处理类
我们之前介绍的,都是读入与写出字节数据,如果处理字符数据,InputStream与OutputStream就得对照编码表,在字符与字节之间进行转换,但是我们有专门的字符处理类,可以帮我们省去这项工作。
如果想从来源读取字符数据或将字符数据写至目的地,可以参考下面的代码:
import java.io.*;
public class CharUtil {
public static void dump(Reader src, Writer dest) throws IOException{
try(Reader input = src; Writer output = dest){
char[] data = new char[1024];
int length;
while((length = input.read(data)) != -1){
output.write(data, 0, length);
}
}
}
}
Reader与Writer继承架构
从继承架构中我们也可以知道,FileReader是一种Reader子类,主要用于读取文档并将读到的数据转换为字符,StringWriter也是一种Writer,可以将字符数据写至StringWriter,最后使用toString方法将字符串取出。所以可以改写上面的代码:
import java.io.*;
public class CharUtilDemo {
public static void main(String[] args) throws IOException{
FileReader reader = new FileReader(args[0]);
StringWriter writer = new StringWriter();
CharUtil.dump(reader, writer);
System.out.println(writer.toString());
}
}
其他子类和之前所讲的基本上都是很类似的,在这里不再赘述。
字符处理装饰器(打包器)
Reader和Writer也有装饰器类,可以对他们的功能进行加强。
InputStreamReader与OutputStreamWriter
在指定InputStreamReader与OutputStreamWriter时,可以指定编码,如果没有指定编码,则以JVM启动时所获取的默认编码来做字符转换。下面将CharUtil的dump进行改写,使它可以指定编码。
import java.io.*;
public class CharUtil2 {
public static void dump(Reader src, Writer dest) throws IOException{
try(Reader input = src; Writer output = dest){
char[] data = new char[1024];
int length;
while((length = input.read(data)) != -1){
output.write(data, 0, length);
}
}
}
public static void dump(InputStream src, OutputStream dest, String charset) throws IOException{
dump(
new InputStreamReader(src, charset),
new OutputStreamWriter(dest, charset)
);
}
//默认编码
public static void dump(InputStream src, OutputStream dest) throws IOException{
dump(src, dest, System.getProperty("file.encoding"));
}
}
BufferedReader与BufferedWriter
与之前的BufferedInputStream与BufferedOutputStream有着相同的功能,用法方面稍有不同。在这里不再赘述。
PrintWriter
与PrintStream极为相似,除了可以对OutputStream打包之外,还可以对Writer打包,提供输出方法。