1.InputStream与OutputStream

学习输入/输出API,一定要先了解Java中如何以串流(Stream)抽象化输入/输出概念,以及InputStream、OutputStream继承架构。

如此一来,无论标准输入/输出、文档输入/输出、网络输入/输出、数据库输入/输出等都可用一致的操作进行处理。

1.1 串流设计的概念

Java将输入/输出抽象化为串流,数据有来源及目的地,而衔接两者的是串流对象,比喻来说,数据就好比水,串流好比水管,通过水管的衔接,水由一端流向另一端。

image-20190627221301947

从应用程序角度来看,如果要将数据从来源取出,可以使用输入串流,如果要将数据写入目的地,可以使用输出串流。

在Java中,输入串流代表对象为java.io.InputStream实例,输出串流代表对象为java.io.OutputStream实例

无论数据源或目的地为何,只要设法取得InputStream或OutputStream的实例,接下来操作输入/输出的方式都是一致,无须理会来源或目的地的真正形式

image-20190627221507270

即使我们不知道最终的来源与目的地,我们也可以设计一个通用方法。

比如

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 = -1;
            while ((length = input.read(data)) != -1) {
                output.write(data, 0, length);
            }
        }
    }
}

下面我们写一个测试类。

public class Copy {
    public static void main(String[] args) throws IOException {
        IO.dump(new FileInputStream(args[0]), new FileOutputStream(args[1]));
    }
}

我们应该如何执行呢?

/Users/jundongpei/Downloads/copy.txt /Users/jundongpei/Downloads/newcopy.txt 

image-20190627222823373

使用我们写的工具类,即使是从HTTP服务器读取某个网页并另存为文档,也是可以使用。

比如

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);
    }
}

然后我们自己指定参数

http://www.ripjava.com /Users/jundongpei/Downloads/ripjava.txt 

那下面我们就来看一下

1.2 串流继承架构

在了解串流抽象化数据源与目的地的概念后,接下来要搞清楚Java中InputStream、OutputStream的继承架构。首先看到InputStream的常用类继承架构。

image-20190627223746530

再来看OutputStream的常用类继承架构。

image-20190627223817583

下面我们分别来看一下

1.2.1标准输入/输出流

还记得System.in与System.out吗?查看API文件的话,会发现它们分别是InputStream与PrintStream的实例,分别代表标准输入(Standard input)与标准输出(Standard output)。

  • System.in

    因为控制台下通常是取得整行用户输入,因此较少直接操作InputStream相关方法,而是如前面学习中使用java.util.Scanner打包System.in,你操作Scanner相关方法,而Scanner会代你操控System.in取得数据,并转换为取得你想要的数据类型。

    我们可以使用System的setIn()方法指定InputStream实例,重新指定标准输入来源。

    比如

    public class StandardIn {
    
        public static void main(String[] args) throws IOException {
            System.setIn(new FileInputStream("/Users/jundongpei/Downloads/copy.txt"));
            try (Scanner scanner = new Scanner(System.in)) {
                while (scanner.hasNextLine()) {
                    System.out.println(scanner.nextLine());
                }
            }
        }
    }
    
  • System.out

    首先,System.out是PrintStream的实例,

    可以使用System的setOut()方法指定PrintStream实例,将结果输出至指定的目的地。

    我们来看一个例子。

    public class StandardOut {
        public static void main(String[] args) throws IOException {
            try (PrintStream printStream = new PrintStream(
                    new FileOutputStream("/Users/jundongpei/Downloads/StandardOutTest.txt "))) {
                System.setOut(printStream);
                System.out.println("HelloWorld");
            }
        }
    }
    

    PrintStream接受InputStream实例,然后用PrintStream打包FileOutputStream,操作PrintStream相关方法,PrintStream会代你操作FileOutputStream。

    对了。我们可以将某些输出至标准输出

    public class DownloadStandardOut {
        public static void main(String[] args) throws IOException {
            URL url = new URL("https://www.baidu.com/");
            InputStream src = url.openStream();
            IO.dump(src, System.out);
        }
    }
    
  • System.err

    System.err为PrintSteam实例,称为标准错误输出串流,它是用来立即显示错误信息。

    我们也可以使用System.setErr()指定PrintStream,重新指定标准错误输出串流。

练习
  1. 使用System.err打印"hello world",并使用System.setErr()方法重定向到一个本地的文件。
1.2.2 FileInputStream与FileOutputStream

FileInputStream是InputStream的子类,可以指定文件名创建实例,一旦创建文档就开启,接着就可用来读取数据。

FileOutputStream是OutputStream的子类,可以指定文件名创建实例,一旦创建文档就开启,接着就可以用来写出数据。

无论FileInputStream还是FileOutputStream,不使用时都要使用close()关闭文档。

FileInputStream主要操作了InputStream的read()抽象方法,使之可从文档中读取数据,

FileOutputStream主要操作了OutputStream的write()抽象方法,使之可写出数据至文档,

前面的IO.dump()方法中已示范过read()与write()方法。

FileInputStream、FileOutputStream在读取、写入文档时,是以字节为单位,

通常会使用一些高阶类加以打包,进行一些高阶操作,比如,前面示范过的Scanner与PrintStream类等。

之后还会看到更多打包InputStream、OutpuStream的类,它们也可以用来打包FileInputStream、FileOutputStream。

练习
  1. 参考前面的IO.dump()方法,使用FileInputStream读取一个文件的内容、通过FileOutputStream写入到某个文档中。
1.2.3 ByteArrayInputStream与ByteArrayOutputStream

ByteArrayInputStream是InputStream的子类,可以指定byte数组创建实例,一旦创建就可将byte数组当作数据源进行读取。

ByteArrayOutputStream是OutputStream的子类,可以指定byte数组创建实例,一旦创建将byte数组当作目的地写出数据。

ByteArrayInputStream主要操作了InputStream的read()抽象方法,使之可从byte数组中读取数据。ByteArrayOutputStream主要操作了OutputStream的write()抽象方法,使之可写出数据至byte数组。

ByteArrayInputStream、ByteArrayOutputStream的操作,和IO.dump()方法里的都一样,

毕竟它们都是InputStream、OutputStream的子类。

1.3 串流处理装饰器

InputStream、OutputStream提供串流基本操作,如果想要为输入/输出的数据做加工处理,则可以使用打包器类。

前面示范过的Scanner类就是作为打包器,其接受InputStream实例,你操作Scanner打包器相关方法,Scanner会实际操作打包的InputStream取得数据,并转换为你想要的数据类型。

InputStream、OutputStream的一些子类也具有打包器的作用,这些子类创建时,可以接受InputStream、OutputStream实例。

前面介绍的PrintStream就是实际例子,你操作PrintStream的print()、println()等方法,PrintStream会自动转换为byte数组数据,利用打包的OutputStream进行输出。

常用的打包器有

  • 具备缓冲区作用的BufferedInputStream、BufferedOutputStream,
  • 具备数据转换处理作用的DataInputStream、DataOutputStream,
  • 具备对象串行化能力的ObjectInputStream、ObjectOutputStream等。

由于这些类本身并没有改变InputStream、OutputStream的行为,只不过在InputStream取得数据之后,再做一些加工处理,或者是要输出时做一些加工处理,再交由OutputStream真正进行输出,因此又称它们为装饰器(Decorator)。

就像照片本身装上华丽外框,就可以让照片感觉更为华丽,或有点像小水管衔接大水管,如小水管(InputStream)读入数据,再由大水管(如BufferedInputStream)增加缓冲功能。

image-20190627232750133

1.3.1 BufferedInputStream与BufferedOutputStream

在前面IO.dump()方法中,每次调用InputStream的read()方法,都会直接向来源要求数据,每次调用OutputStream的write()方法时,都会直接将数据写到目的地,这并不是个有效率的方式。

以文档存取为例,如果传入IO.dump()的是FileInputStream、FileOutputStream实例,每次read()时都会要求读取硬盘,每次write()时都会要求写入硬盘,这会花费许多时间在硬盘定位上。

如果InputStream第一次read()时可以尽量读取足够的数据至内存的缓冲区,后续调用read()时先看看缓冲区是不是还有数据,如果有就从缓冲区读取,没有再从来源读取数据至缓冲区,这样减少从来源直接读取数据的次数,对读取效率将会有帮助(毕竟内存的访问速度较快)。

如果OutputStream每次write()时可将数据写入内存中的缓冲区,缓冲区满了再将缓冲区的数据写入目的地,这样可减少对目的地的写入次数,对写入效率将会有帮助。

BufferedInputStream与BufferedOutputStream提供的就是前面描述的缓冲区功能,创建BufferedInputStream、BufferedOutputStream必须提供InputStream、OutputStream进行打包,可以使用默认或自定义缓冲区大小。

BufferedInputStream与BufferedOutputStream主要在内部提供缓冲区功能,操作上与InputStream、OutputStream并没有太大差别。例如,改写前面的IO.dump()为BufferedIO.dump()方法:

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 = -1;
            while ((length = input.read(data)) != -1) {
                output.write(data, 0, length);
            }
        }
    }
}
1.3.2 DataInputStream与DataOutputStream

DataInputStream、DataOutputStream用来装饰InputStream、OutputStream,DataInputStream、DataOutputStream提供读取、写入Java基本数据类型的方法,像是读写int、double、boolean等的方法。这些方法会自动在指定的类型与字节间转换,不用你亲自做字节与类型转换的动作。

来看个实际使用DataInputStream、DataOutputStream的例子。下面的Member类可以调用save()储存Member实例本身的数据,文件名为Member的会员号码,调用Member.load()指定会员号码,则可以读取文档中的会员数据,封装为Member实例并返回:

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;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }
    
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
    
    @Override
    public String toString() {
        return String.format("(%s, %s, %d)", number, name, age);
    }
    
    public void save() {
        try(DataOutputStream output = 
                new DataOutputStream(new FileOutputStream(number))) {
            output.writeUTF(number);
            output.writeUTF(name);
            output.writeInt(age);
        } catch(IOException ex) {
            throw new RuntimeException(ex);
        }
    }
    
    public static Member load(String number) {
        Member member = null;
        try(DataInputStream input = 
                new DataInputStream(new FileInputStream(number))) {
            member = new Member(
                    input.readUTF(), input.readUTF(), input.readInt());
        } catch(IOException ex) {
            throw new RuntimeException(ex);
        }
        return member;
    }
}
1.3.3 ObjectInputStream与ObjectOutputStream

前面的范例是取得Member的number、name、age数据进行储存,读回时也是先取得number、name、age数据再用来创建Member实例。实际上,也可以将内存中的对象整个储存下来,之后再读入还原为对象。可以使用ObjectInputStream、ObjectOutputStream装饰InputStream、OutputStream来完成这项工作。

ObjectInputStream提供readObject()方法将数据读入为对象,而ObjectOutputStream提供writeObject()方法将对象写至目的地,可以被这两个方法处理的对象,必须操作java.io.Serializable接口,这个接口并没有定义任何方法,只是作为标示之用,表示这个对象是可以串行化的(Serializable)。

下面这个范例改写前一个范例,使用ObjectInputStream、ObjectOutputStream来储存、读入数据:

public class Member2 implements 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;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }
    
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
    
    @Override
    public String toString() {
        return String.format("(%s, %s, %d)", number, name, age);
    }
    
    public void save() {
        try(ObjectOutputStream output = 
                new ObjectOutputStream(new FileOutputStream(number))) {
            output.writeObject(this);
        } catch(IOException ex) {
            throw new RuntimeException(ex);
        }
    }
    
    public static Member2 load(String number) {
        Member2 member = null;
        try(ObjectInputStream input = 
                new ObjectInputStream(new FileInputStream(number))) {
            member = (Member2) input.readObject(); 
        } catch(IOException | ClassNotFoundException ex) {
            throw new RuntimeException(ex);
        }
        return member;
    }
}

如果在做对象串行化时,对象中某些数据成员不希望被写出,则可以标上**transient****关键字。

1.4 字符处理类

InputStream、OutputStream是用来读入与写出字节数据,若实际上处理的是字符数据,使用InputStream、OutputStream就得对照编码表,在字符与字节之间进行转换。所幸Java SE API已提供相关输入/输出字符处理类,让你不用亲自进行字节与字符编码转换的枯燥工作。

1.4.1 Reader与Writer继承架构

针对字符数据的读取,Java SE提供了java.io.Reader类,其抽象化了字符数据读入的来源。针对字符数据的写入,则提供了java.io.Writer类,其抽象化了数据写出的目的地。

举个例子来说,如果想从来源读入字符数据,或将字符数据写至目的地,都可以使用下面的CharUtil.dump()方法:

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 = 0;
            while((length = input.read(data)) != -1) {
                output.write(data, 0, length);
            }
        }
    }
}

在不使用Reader与Writer时,必须使用close()方法关闭串流。

看Reader继承架构

image-20190627234613857

看Writer常用类继承架构

image-20190627234642184

FileReader是一种Reader,主要用于读取文档并将读到的数据转换为字符;StringWriter是一种Writer,可以将字符数据写至StringWriter,最后使用toString()方法取得字符串,代表所有写入的字符数据。所以,若要使用CharUtil.dump()读入文档、转为字符串并显示在文本模式中,可以如下:

public class CharUtilDemo {
    public static void main(String[] args) throws IOException {
        FileReader reader = new FileReader("/Users/jundongpei/Downloads/copy.txt");
        StringWriter writer = new StringWriter();
        CharUtil.dump(reader, writer);
        System.out.println(writer.toString());
    }
}

如果执行CharUtilDemo时,在命令行自变量指定了文档位置,若文档中实际都是字符数据,就可以在文本模式中看到文档中的文字内容。

稍微解释一下几个常用的Reader、Writer子类。StringReader可以将字符串打包,当作读取来源,StringWriter则可以作为写入目的地,最后用toString()取得所有写入的字符组成的字符串。CharArrayReader、CharArrayWriter则类似,将char数组当作读取来源以及写入目的地。

FileReader、FileWriter可以对文档做读取与写入,读取或写入时默认会使用操作系统默认编码来做字符转换。也就是说,如果你的操作系统默认编码是GB2312,则FileReader、FileWriter会以GB2312对你的“纯文本文档”做读取、写入的动作,如果操作系统默认编码是UTF-8,则FileReader、FileWriter就使用UTF-8。

在启动JVM时,可以指定-Dfile.encoding来指定FileReader、FileWriter所使用的编码。例如,指定使用UTF-8:

java -dfile.encoding=UTF-8

FileReader、FileWriter没有可以指定编码的方法。如果在程序执行过程中想要指定编码,则必须使用InputStreamReader、OutputStreamWriter,这两个类实际上是作为装饰器。

1.5 字符处理装饰器

正如同InputStream、OutputStream有一些装饰器类,可以对InputStream、OutputStream打包增加额外功能,Reader、Writer也有一些装饰器类可供使用。下面介绍常用的字符处理装饰器类。

1.5.1 InputStreamReader与OutputStreamWriter

如果串流处理的字节数据,实际上代表某些字符的编码数据,而你想要将这些字节数据转换为对应的编码字符,可以使用InputStreamReader、OutputStreamWriter对串流数据打包。

在建立InputStreamReader与OutputStreamWriter时,可以指定编码,如果没有指定编码,则以JVM启动时所获取的默认编码来做字符转换。下面将CharUtil的dump()改写,提供可指定编码的dump()方法:

    public static void dump(Reader src, Writer dest) throws IOException {
        try(Reader input = src; Writer output = dest) {
            char[] data = new char[1024];
            int length = 0;
            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)
        );
    }

如果想以UTF-8处理字符数据,例如读取UTF-8的Main.java文本文件,并另存为UTF-8的Main.txt文本文件,则可以如下:

		CharUtil2.dump(
				new FileInputStream("Main.java"), 
				new FileOutputStream("Main.txt"), 
				"UTF-8");
1.5.2 BufferedReader与BufferedWriter

正如BufferedInputStream、BufferedOutputStream为InputStream、OutputStream提供缓冲区作用,以改进输入/输出的效率,BufferedReader、BufferedWriter可对Reader、Writer提供缓冲区作用,在处理字符输入/输出时,对效率也会有所帮助。

举个使用BufferedReader的例子。在JDK 1.4之前,标准API并没有Scanner类,若要在文本模式下取得用户输入的字符串,会这样撰写:

public class BufferedReaderDemo {
	public static void main(String[] args) throws Exception {
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));) {
			String some = reader.readLine();
			System.out.printf("print: %s", some);
		}
	}
}

创建BufferedReader时要指定被打包的Reader,可以指定或采用默认缓冲区大小。就API的使用而言,System.in是InputStream实例,可以指定给InputStreamReader创建之用,InputStreamReader是一种Reader,所以可指定给BufferedReader创建之用。

就装饰器的作用而言,InputStreamReader将System.in读入的字节数据做编码转换,而BufferedReader将编码转换后的数据做缓冲处理,以增加读取效率。BufferedReader的readLine()方法,可以读取一行数据(以换行字符为依据)并以字符串返回,返回的字符串不包括换行字符。

1.5.3 PrintWriter

PrintWriter与PrintStream使用上极为类似,不过除了可以对OutputStream打包之外,PrintWriter还可以对Writer进行打包,提供print()、println()、format()等方法。